第十戒: Panel と転生 〜 コンポーネントに永遠の命を与えよ
Wicket の強みのひとつは、UI コンポーネントを真に再利用可能な形で作成できることです。 Panel、Fragment、Border を使い分けることで、 保守性が高く一貫した UI を構築できます。
再利用可能コンポーネントの種類
| 種類 | 特徴 | 用途 |
|---|---|---|
Panel |
独自の HTML ファイルを持つ | 独立した再利用コンポーネント |
Fragment |
親ページの HTML 内に定義 | 条件分岐による表示切り替え |
Border |
子コンテンツを囲む装飾 | 共通レイアウト、装飾枠 |
Panel
Panel は、独自の HTML マークアップファイルを持つ再利用可能なコンポーネントです。
WebPage と同様に、Java クラスと同名・同パッケージの HTML ファイルがペアになります。
Panel の基本構造
public class UserInfoPanel extends Panel {
public UserInfoPanel(String id,
IModel<User> userModel) {
super(id, userModel);
add(new Label("name",
new PropertyModel<>(userModel, "name")));
add(new Label("email",
new PropertyModel<>(userModel, "email")));
add(new Label("department",
new PropertyModel<>(userModel, "department")));
}
}
<wicket:panel>
<div class="user-info">
<h3>ユーザー情報</h3>
<dl>
<dt>名前</dt>
<dd wicket:id="name">名前</dd>
<dt>メール</dt>
<dd wicket:id="email">メール</dd>
<dt>部署</dt>
<dd wicket:id="department">部署</dd>
</dl>
</div>
</wicket:panel>
Panel の HTML では、必ず <wicket:panel> タグで囲む必要があります。
このタグの外にある HTML は無視されます(デザインプレビュー用に使えます)。
Panel の使い方(親ページ側)
public class UserPage extends WebPage {
public UserPage() {
User user = userService.findById(1);
add(new UserInfoPanel("userPanel",
Model.of(user)));
}
}
<html xmlns:wicket="http://wicket.apache.org">
<body>
<h1>ユーザー詳細</h1>
<!-- Panel はdivなどのタグに配置 -->
<div wicket:id="userPanel">
ここに Panel の内容が挿入される
</div>
</body>
</html>
実践例: アドレスパネル
住所入力フォームを Panel として再利用可能にする例です。
public class AddressPanel extends Panel {
public AddressPanel(String id, IModel<Address> model) {
super(id, new CompoundPropertyModel<>(model));
add(new TextField<>("postalCode")
.setRequired(true)
.add(StringValidator.exactLength(7)));
add(new DropDownChoice<>("prefecture",
Arrays.asList("東京都", "大阪府", "愛知県")));
add(new TextField<>("city").setRequired(true));
add(new TextField<>("street"));
}
}
<wicket:panel>
<div class="address-form">
<div>
<label>郵便番号:
<input wicket:id="postalCode" type="text"/>
</label>
</div>
<div>
<label>都道府県:
<select wicket:id="prefecture"></select>
</label>
</div>
<div>
<label>市区町村:
<input wicket:id="city" type="text"/>
</label>
</div>
<div>
<label>番地:
<input wicket:id="street" type="text"/>
</label>
</div>
</div>
</wicket:panel>
このパネルは複数のページで再利用できます:
// ユーザー登録ページで使う
form.add(new AddressPanel("homeAddress", homeAddressModel));
// 注文ページで配送先と請求先の両方に使う
form.add(new AddressPanel("shippingAddress", shippingModel));
form.add(new AddressPanel("billingAddress", billingModel));
Panel へのパラメータ受け渡し
Panel にモデル以外のパラメータを渡すには、コンストラクタで受け取ります。
public class UserInfoPanel extends Panel {
public UserInfoPanel(String id,
IModel<User> userModel,
boolean showEmail) {
super(id, userModel);
// ... コンポーネント追加
emailLabel.setVisible(showEmail);
}
}
Fragment
Fragment は、親ページ(または Panel)の HTML 内に定義されたマークアップの断片を使うコンポーネントです。
独自の HTML ファイルを持たず、<wicket:fragment> タグで定義します。
条件に応じて表示内容を切り替える場合に便利です。
public class StatusPage extends WebPage {
public StatusPage() {
boolean isLoggedIn = /* ... */;
if (isLoggedIn) {
add(new Fragment("content",
"loggedInFragment", this));
} else {
add(new Fragment("content",
"guestFragment", this));
}
}
}
<html xmlns:wicket="http://wicket.apache.org">
<body>
<!-- ここにFragmentが挿入される -->
<div wicket:id="content"></div>
<!-- Fragment の定義 -->
<wicket:fragment
wicket:id="loggedInFragment">
<p>ようこそ!ログイン中です。</p>
<a href="#">マイページ</a>
</wicket:fragment>
<wicket:fragment
wicket:id="guestFragment">
<p>ゲストユーザーです。</p>
<a href="#">ログイン</a>
</wicket:fragment>
</body>
</html>
Fragment のコンストラクタは3つの引数を取ります:
- 第1引数: 挿入先の wicket:id(
"content") - 第2引数: Fragment定義の wicket:id(
"loggedInFragment") - 第3引数: Fragment定義を含むマークアップコンテナ(通常は
this)
Fragment は独自の HTML ファイルが不要なので、小さな表示切り替えに適しています。 大きなコンポーネントには Panel を使いましょう。
Border
Border は、子コンテンツを囲む「枠」を提供するコンポーネントです。
共通のヘッダー/フッター、パネルの装飾、レイアウトの統一に使えます。
public class RoundedBoxBorder extends Border {
public RoundedBoxBorder(String id,
IModel<String> titleModel) {
super(id);
addToBorder(new Label("title", titleModel));
}
}
<wicket:border>
<div class="rounded-box">
<h3 wicket:id="title">タイトル</h3>
<div class="box-content">
<!-- 子コンテンツがここに挿入される -->
<wicket:body/>
</div>
</div>
</wicket:border>
使い方:
RoundedBoxBorder box = new RoundedBoxBorder(
"infoBox", Model.of("お知らせ"));
add(box);
// Border の子コンテンツとして追加
box.add(new Label("message",
"重要なお知らせです。"));
<div wicket:id="infoBox">
<p wicket:id="message">
メッセージ
</p>
</div>
Border のポイント:
<wicket:body/>の位置に、親ページで wicket:id 内に書いた子コンテンツが挿入されます- Border 自身のコンポーネントは
addToBorder()で追加します - 子コンテンツのコンポーネントは通常の
add()で追加します
マークアップ継承
Wicket は Java のクラス継承に合わせて、HTML マークアップも継承できます。 共通レイアウト(ヘッダー、フッター、ナビゲーション)を持つベースページを作成し、 個別ページはコンテンツ部分だけを定義できます。
public abstract class BasePage extends WebPage {
public BasePage() {
add(new Label("pageTitle", getPageTitle()));
add(new BookmarkablePageLink<>("homeLink", HomePage.class));
add(new BookmarkablePageLink<>("aboutLink", AboutPage.class));
}
protected abstract String getPageTitle();
}
<html xmlns:wicket="http://wicket.apache.org">
<head>
<title wicket:id="pageTitle">タイトル</title>
</head>
<body>
<nav>
<a wicket:id="homeLink">聖堂</a>
<a wicket:id="aboutLink">About</a>
</nav>
<main>
<!-- 子ページのコンテンツがここに挿入される -->
<wicket:child/>
</main>
<footer>© 2025 My App</footer>
</body>
</html>
public class HomePage extends BasePage {
public HomePage() {
add(new Label("welcome",
"ようこそ!"));
}
@Override
protected String getPageTitle() {
return "ホーム";
}
}
<wicket:extend>
<h1 wicket:id="welcome">ようこそ</h1>
<p>ここがホームページです。</p>
</wicket:extend>
ベースページの <wicket:child/> の位置に、
子ページの <wicket:extend> の内容が挿入されます。
Enclosure
<wicket:enclosure> を使うと、
内部のコンポーネントが非表示の場合に、囲んだ HTML 全体を非表示にできます。
<!-- emailLabel が非表示の場合、label タグごと非表示になる -->
<wicket:enclosure child="email">
<label>メール: <span wicket:id="email"></span></label>
</wicket:enclosure>
child 属性で指定したコンポーネントの isVisible() が false の場合、
<wicket:enclosure> 全体が描画されません。
再利用のベストプラクティス
- Panel: 独立した機能を持つ再利用コンポーネントに使う(ユーザーカード、検索フォーム、グラフ等)
- Fragment: 同じページ内で条件分岐による表示切り替えに使う
- Border: 共通の外枠・装飾が必要な場合に使う
- マークアップ継承: サイト全体の共通レイアウトに使う
- Behavior の秘術: 見た目の装飾やイベント処理の追加には Behavior を使う(第9章参照)
「このコンポーネントは別のプロジェクトでも使えるか?」と自問してみましょう。 「はい」なら Panel にする価値があります。 「このページ内だけで使う」なら Fragment で十分です。
実務でハマりやすい注意点
-
問題Panel 内で非表示にした部品を再表示しようとしても、Ajax 更新で戻ってこない。操作したのに画面が変わらない状態になる。原因非表示時に再描画先が消えており、placeholder 設定が不足している。復帰時に差し替えるDOMが存在しない。対策表示切替対象は placeholder 出力を有効化する。Panel の再利用部品ほど、この前提を最初に揃えておく。
-
問題Panel に分割した途端、submit先やバリデーションの挙動が不安定になる。画面構成変更後に発生しやすい。原因フォーム構造が入れ子になり、HTML仕様に反している。ブラウザ解釈に任せる状態になってしまう。対策フォームはフラットに保ち、UIの分割だけ Panel/Fragment で行う。設計時にフォーム境界を先に固定する。