第13章: ModalDialog とポップアップ検索
業務システムでは、取引先、商品、部門、法人番号などを別ウインドウで検索し、
選択した値を元の入力画面へ戻す「ポップアップ検索」がよく使われます。
Wicket 10 では ModalDialog を使うことで、このような UI を Wicket コンポーネントとして実装できます。
ModalDialog の用途
ModalDialog は、ページ上にモーダル領域を表示し、その中に任意の Wicket コンポーネントを差し込むためのコンポーネントです。
業務画面では、次のような用途で特に便利です。
- コード入力欄に対する検索候補の表示
- 検索結果一覧からの値選択
- 確認ダイアログより複雑な入力を伴う補助画面
- ページ遷移せずにマスタ参照や候補選択を完結させたい場合
単純な確認だけなら JavaScript の confirm() で足ります。
しかし、検索条件、検索結果、選択ボタン、Ajax 更新を含む画面では、Wicket コンポーネントとして作る方が保守しやすくなります。
依存関係
ModalDialog は wicket-core ではなく wicket-extensions に含まれます。
Maven プロジェクトでは次の依存関係を追加します。
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-extensions</artifactId>
<version>10.8.0</version>
</dependency>
Java 側では次のクラスを使います。
import org.apache.wicket.extensions.ajax.markup.html.modal.ModalDialog;
import org.apache.wicket.extensions.ajax.markup.html.modal.theme.DefaultTheme;
基本構成
モーダルを開く親ページには、ModalDialog 本体と、モーダルを開く Ajax コンポーネントを配置します。
ダイアログの中身は Panel として別クラスに切り出すと再利用しやすくなります。
親ページ HTML
<form wicket:id="form">
<input type="text" wicket:id="txtCode" />
<button type="button" wicket:id="btnSearch">検索</button>
<div wicket:id="searchDialog"></div>
</form>
親ページ Java
private ModalDialog searchDialog;
private TextField<String> txtCode;
txtCode = new TextField<>("txtCode", new PropertyModel<>(bean, "code"));
txtCode.setOutputMarkupId(true);
form.add(txtCode);
searchDialog = new ModalDialog("searchDialog");
searchDialog.add(new DefaultTheme());
searchDialog.closeOnEscape().trapFocus();
form.add(searchDialog);
AjaxButton btnSearch = new AjaxButton("btnSearch") {
@Override
protected void onSubmit(AjaxRequestTarget target) {
SearchPanel panel = new SearchPanel(
ModalDialog.CONTENT_ID,
searchDialog,
(ajaxTarget, selectedCode) -> {
bean.setCode(selectedCode);
txtCode.setModelObject(selectedCode);
ajaxTarget.add(txtCode);
});
searchDialog.open(panel, target);
}
};
btnSearch.setDefaultFormProcessing(false);
form.add(btnSearch);
元画面の入力欄を Ajax で更新する場合は setOutputMarkupId(true) が必要です。
これを忘れると target.add(txtCode) してもブラウザ側を更新できません。
ポップアップ検索の実装
検索ダイアログは Panel として実装します。
検索条件、結果一覧、選択ボタンを持ち、選択時に親ページへ値を返します。
検索パネル HTML
<wicket:panel>
<div class="popup-search">
<a href="#" wicket:id="btnClose">閉じる</a>
<input type="text" wicket:id="txtKeyword" />
<a href="#" wicket:id="btnExecuteSearch">検索</a>
<table wicket:id="resultContainer">
<tr wicket:id="listViewResult">
<td><span wicket:id="lblCode"></span></td>
<td><span wicket:id="lblName"></span></td>
<td><a href="#" wicket:id="btnSelect">選択</a></td>
</tr>
</table>
</div>
</wicket:panel>
検索パネル Java
public class SearchPanel extends Panel {
private static final long serialVersionUID = 1L;
private final ModalDialog dialog;
private final SelectHandler selectHandler;
private String keyword;
private List<Candidate> results = new ArrayList<>();
private WebMarkupContainer resultContainer;
private ListView<Candidate> listViewResult;
public SearchPanel(String id, ModalDialog dialog, SelectHandler selectHandler) {
super(id);
this.dialog = dialog;
this.selectHandler = selectHandler;
initForm();
}
private void initForm() {
add(new AjaxLink<Void>("btnClose") {
@Override
public void onClick(AjaxRequestTarget target) {
dialog.close(target);
}
});
add(new TextField<>("txtKeyword", new PropertyModel<>(this, "keyword")));
add(new AjaxLink<Void>("btnExecuteSearch") {
@Override
public void onClick(AjaxRequestTarget target) {
results = search(keyword);
listViewResult.setList(results);
listViewResult.removeAll();
target.add(resultContainer);
}
});
resultContainer = new WebMarkupContainer("resultContainer");
resultContainer.setOutputMarkupId(true);
add(resultContainer);
listViewResult = new ListView<>("listViewResult", results) {
@Override
protected void populateItem(ListItem<Candidate> item) {
Candidate candidate = item.getModelObject();
item.add(new Label("lblCode", candidate.code()));
item.add(new Label("lblName", candidate.name()));
item.add(new AjaxLink<Void>("btnSelect") {
@Override
public void onClick(AjaxRequestTarget target) {
selectHandler.onSelect(target, candidate.code());
dialog.close(target);
}
});
}
};
resultContainer.add(listViewResult);
}
@FunctionalInterface
public interface SelectHandler extends Serializable {
void onSelect(AjaxRequestTarget target, String selectedCode);
}
}
モーダル内の検索や選択は、外側フォームの送信と混ざらないように AjaxLink を使うと扱いやすくなります。
AjaxButton を使う場合は setDefaultFormProcessing(false) を明示して、
元画面のバリデーションを走らせないようにします。
シリアライズの注意点
Wicket はページインスタンスをセッションやページストアへ保存するため、ページが保持するコンポーネントやフィールドは シリアライズ可能である必要があります。ポップアップ検索でよく問題になるのは、選択時コールバックです。
@FunctionalInterface
public interface SelectHandler extends Serializable {
void onSelect(AjaxRequestTarget target, String selectedCode);
}
ラムダ式をフィールドに保持する場合でも、代入先の関数型インターフェースが Serializable を継承していれば、
Wicket のシリアライズチェックに通せます。
NotSerializableException が出た場合は、スタックトレースの
Field hierarchy を読み、どのフィールドに非 Serializable オブジェクトが残っているか確認します。
DAO、EntityManager、Service 実体などを Panel のフィールドに持つのも避けましょう。
Bootstrap との衝突
Wicket の ModalDialog は内部で modal-dialog という CSS クラスを出力します。
Bootstrap も同じクラス名を使っており、Bootstrap 側の指定によってクリックやスクロールが効かなくなることがあります。
代表例は pointer-events: none の衝突です。
.modal-dialog-overlay {
position: fixed;
inset: 0;
overflow: auto;
pointer-events: auto;
z-index: 1055;
}
.modal-dialog-overlay .modal-dialog {
pointer-events: auto !important;
max-width: none;
}
Bootstrap を併用する場合は、Wicket の ModalDialog 用 CSS を明示しておくと安全です。 クリックできない、入力欄にマウスでフォーカスできない、モーダル内でスクロールできない、といった症状は CSS 衝突を疑います。
実務でハマりやすい注意点
-
問題検索ボタンを押すと元画面の必須入力エラーが出る。原因モーダル内ボタンが外側フォームの通常送信として扱われ、元画面のバリデーションが実行されている。対策
AjaxLinkを使うか、AjaxButtonにsetDefaultFormProcessing(false)を設定する。 -
問題選択しても元画面の入力欄が更新されない。原因更新対象コンポーネントに markup id が出力されていない、または Ajax レスポンスに追加していない。対策対象入力欄へ
setOutputMarkupId(true)を設定し、選択時にtarget.add(component)する。 -
問題ページ保存時に
NotSerializableExceptionが発生する。原因検索パネルが保持するラムダ、サービス、DAO などが Serializable ではない。対策コールバック用インターフェースはSerializableを継承する。永続化リソースはフィールドに保持しない。 -
問題入力欄をクリックできず、モーダル内スクロールも効かない。原因Bootstrap の
.modal-dialogと Wicket の.modal-dialogが衝突している。対策ModalDialog 配下のpointer-eventsとoverflowを CSS で明示的に上書きする。
まとめ
| 観点 | ポイント |
|---|---|
| 依存関係 | wicket-extensions を追加する |
| 構成 | 親ページに ModalDialog、中身は Panel に分離する |
| 値の返却 | Serializable なコールバックで親ページへ通知する |
| Ajax 更新 | 更新対象に setOutputMarkupId(true) を設定する |
| CSS | Bootstrap 併用時は .modal-dialog の衝突に注意する |