第10章: Panel と再利用可能コンポーネント

Wicket の強みのひとつは、UI コンポーネントを真に再利用可能な形で作成できることです。 Panel、Fragment、Border を使い分けることで、 保守性が高く一貫した UI を構築できます。

再利用可能コンポーネントの種類

種類特徴用途
Panel 独自の HTML ファイルを持つ 独立した再利用コンポーネント
Fragment 親ページの HTML 内に定義 条件分岐による表示切り替え
Border 子コンテンツを囲む装飾 共通レイアウト、装飾枠

Panel

Panel は、独自の HTML マークアップファイルを持つ再利用可能なコンポーネントです。 WebPage と同様に、Java クラスと同名・同パッケージの HTML ファイルがペアになります。

Panel の基本構造

UserInfoPanel.java
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")));
    }
}
UserInfoPanel.html
<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 の使い方(親ページ側)

UserPage.java
public class UserPage extends WebPage {
    public UserPage() {
        User user = userService.findById(1);
        add(new UserInfoPanel("userPanel",
            Model.of(user)));
    }
}
UserPage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <h1>ユーザー詳細</h1>
  <!-- Panel はdivなどのタグに配置 -->
  <div wicket:id="userPanel">
    ここに Panel の内容が挿入される
  </div>
</body>
</html>

実践例: アドレスパネル

住所入力フォームを Panel として再利用可能にする例です。

AddressPanel.java
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"));
    }
}
AddressPanel.html
<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> タグで定義します。

条件に応じて表示内容を切り替える場合に便利です。

StatusPage.java
public class StatusPage extends WebPage {
    public StatusPage() {
        boolean isLoggedIn = /* ... */;

        if (isLoggedIn) {
            add(new Fragment("content",
                "loggedInFragment", this));
        } else {
            add(new Fragment("content",
                "guestFragment", this));
        }
    }
}
StatusPage.html
<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つの引数を取ります:

Fragment は独自の HTML ファイルが不要なので、小さな表示切り替えに適しています。 大きなコンポーネントには Panel を使いましょう。

Border

Border は、子コンテンツを囲む「枠」を提供するコンポーネントです。 共通のヘッダー/フッター、パネルの装飾、レイアウトの統一に使えます。

RoundedBoxBorder.java
public class RoundedBoxBorder extends Border {

    public RoundedBoxBorder(String id,
                           IModel<String> titleModel) {
        super(id);
        addToBorder(new Label("title", titleModel));
    }
}
RoundedBoxBorder.html
<wicket:border>
  <div class="rounded-box">
    <h3 wicket:id="title">タイトル</h3>
    <div class="box-content">
      <!-- 子コンテンツがここに挿入される -->
      <wicket:body/>
    </div>
  </div>
</wicket:border>

使い方:

SomePage.java
RoundedBoxBorder box = new RoundedBoxBorder(
    "infoBox", Model.of("お知らせ"));
add(box);

// Border の子コンテンツとして追加
box.add(new Label("message",
    "重要なお知らせです。"));
SomePage.html
<div wicket:id="infoBox">
  <p wicket:id="message">
    メッセージ
  </p>
</div>

Border のポイント:

  • <wicket:body/> の位置に、親ページで wicket:id 内に書いた子コンテンツが挿入されます
  • Border 自身のコンポーネントは addToBorder() で追加します
  • 子コンテンツのコンポーネントは通常の add() で追加します

マークアップ継承

Wicket は Java のクラス継承に合わせて、HTML マークアップも継承できます。 共通レイアウト(ヘッダー、フッター、ナビゲーション)を持つベースページを作成し、 個別ページはコンテンツ部分だけを定義できます。

BasePage.java
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();
}
BasePage.html
<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>&copy; 2025 My App</footer>
</body>
</html>
HomePage.java
public class HomePage extends BasePage {
    public HomePage() {
        add(new Label("welcome",
            "ようこそ!"));
    }

    @Override
    protected String getPageTitle() {
        return "ホーム";
    }
}
HomePage.html
<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 で十分です。

実務でハマりやすい注意点

  • 問題
    Panel 内で非表示にした部品を再表示しようとしても、Ajax 更新で戻ってこない。操作したのに画面が変わらない状態になる。
    原因
    非表示時に再描画先が消えており、placeholder 設定が不足している。復帰時に差し替えるDOMが存在しない。
    対策
    表示切替対象は placeholder 出力を有効化する。Panel の再利用部品ほど、この前提を最初に揃えておく。
  • 問題
    Panel に分割した途端、submit先やバリデーションの挙動が不安定になる。画面構成変更後に発生しやすい。
    原因
    フォーム構造が入れ子になり、HTML仕様に反している。ブラウザ解釈に任せる状態になってしまう。
    対策
    フォームはフラットに保ち、UIの分割だけ Panel/Fragment で行う。設計時にフォーム境界を先に固定する。