Selectでoptgroupで動的で

プルダウンでoptgroupが使いたい場合、DropDownChoiceは使えないわけで、
org.apache.wicket.extensions.markup.html.form.select
パッケージのクラス群を使えとなっているのだけど、サンプルを見てもコードを見てもプルダウンの内容が動的に変わる場合、キビシイ。需要がないのかなあ?

ぐぐってみると、幾つか解決策がひっかかるのだけれども、あまり気に入らなかったので自作。


まず、グループの箱。インナークラス化したほうが良いやもしれない。

public class OptionGroup<T> implements Serializable{

	private static final long serialVersionUID = 1L;

	/** optgroupのラベル */
	private String groupName;

	/** グループ内の選択肢 */
	private List<T> options;

	public OptionGroup(String groupName) {
		this.groupName = groupName;
	}

	public OptionGroup(String groupName, List<T> options) {
		this.groupName = groupName;
		this.options = options;
	}

	public String getGroupName() {
		return groupName;
	}

	public void setGroupName(String groupName) {
		this.groupName = groupName;
	}

	public List<T> getOptions() {
		return options;
	}

	public void setOptions(List<T> options) {
		this.options = options;
	}

	public void addOptionData(T data) {
		if(options == null) {
			options = new ArrayList<T>();
		}
		options.add(data);
	}
}


次に SelectOptions クラスの代わり。モデルの型の問題で SelectOptions の継承では実現できなかったので、その親の RepeatingView を継承したけど、ほぼ SelectOptions のコードのまま。

public class GroupableSelectOptions<T> extends RepeatingView {

  private static final long serialVersionUID = 1L;

  private boolean recreateChoices = false;

  private final IOptionRenderer<T> renderer;

  public GroupableSelectOptions(String id, IModel<Collection<OptionGroup<T>>> model, IOptionRenderer<T> renderer) {
    super(id, model);
    this.renderer = renderer;
    setRenderBodyOnly(true);
  }

  public GroupableSelectOptions(String id, Collection<OptionGroup<T>> groups, IOptionRenderer<T> renderer) {
    super(id, new WildcardCollectionModel<OptionGroup<? extends T>>(groups));
    this.renderer = renderer;
  }

  public GroupableSelectOptions<T> setRecreateChoices(boolean refresh) {
    recreateChoices = refresh;
    return this;
  }

  @SuppressWarnings("unchecked")
  @Override
  protected final void onPopulate() {
    if (size() == 0 || recreateChoices) {
      removeAll();
      Collection<OptionGroup<T>> modelObject = (Collection<OptionGroup<T>>) getDefaultModelObject();
      if (modelObject != null) {
        if (!(modelObject instanceof Collection)) {
          throw new WicketRuntimeException("Model object " + modelObject + " not a collection");
        }
        for (OptionGroup<T> group : modelObject) {
          int lastIndex = group.getOptions().size() - 1;
          for (int i = 0; i < group.getOptions().size(); i++) {
            T value = group.getOptions().get(i);
            WebMarkupContainer row = new WebMarkupContainer(newChildId());
            row.setRenderBodyOnly(true);
            add(row);
            boolean isStart = (i == 0);
            boolean isEnd = (i == lastIndex);
            row.add(newOption(group, value, isStart, isEnd));
          }
        }
      }
    }
  }

  protected SelectOption<T> newOption(OptionGroup<? extends T> group, T value, boolean isGroupStart, boolean isGroupEnd) {
    IModel<T> model = renderer.getModel(value);
    String text = renderer.getDisplayValue(value);
    GroupSelectOption<T> option = new GroupSelectOption<T>("option", model, text);
    option.isGroupStart = isGroupStart;
    option.isGroupEnd = isGroupEnd;
    if(isGroupStart) {
      option.groupName = group.getGroupName();
    }
    return option;
  }

  private static class GroupSelectOption<V> extends SelectOption<V> {

    private static final long serialVersionUID = 1L;

    private String groupName;

    private boolean isGroupStart = false;
    private boolean isGroupEnd = false;

    private String text;

    public GroupSelectOption(String id, IModel<V> model, String text) {
      super(id, model);
      this.text = text;
    }

    @Override
    protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
      replaceComponentTagBody(markupStream, openTag, text);
    }

    //これがしたかったのだけど、ずいぶん遠回りしたもんだ
    @Override
    protected void onRender(MarkupStream markupStream) {
      if (isGroupStart) {
        getResponse().write("<optgroup label=\"" + groupName + "\">" + groupName + "\n");
      }
      super.onRender(markupStream);
      if (isGroupEnd) {
        getResponse().write("</optgroup>\n");
      }
    }
  }
}


最後に使い方。ページクラスの方。

@SuppressWarnings("serial")
public class OptgroupPage extends WebPage implements Serializable{

  private static List<OptionGroup<Prefecture>> prefectures;

  // 県のデータ
  static class Prefecture implements Serializable {
    String code;
    String name;
    Prefecture(String code, String name) {
      this.code = code;
      this.name = name;
    }
  }

  // 選択肢の初期化
  static {
    prefectures = new ArrayList<OptionGroup<Prefecture>>();
    OptionGroup<Prefecture> touhoku = new OptionGroup<Prefecture>("東北");
    touhoku.addOptionData(new Prefecture("02", "青森県"));
    touhoku.addOptionData(new Prefecture("03", "岩手県"));
    touhoku.addOptionData(new Prefecture("04", "宮城県"));
    prefectures.add(touhoku);
    OptionGroup<Prefecture> kanto = new OptionGroup<Prefecture>("関東");
    kanto.addOptionData(new Prefecture("13", "東京都"));
    kanto.addOptionData(new Prefecture("14", "神奈川県"));
    prefectures.add(kanto);
  }

  public OptgroupPage() {
    super();

    final Label display = new Label("display", Model.of("未選択"));
    add(display);

    Form<Void> form = new Form<Void>("form");
    add(form);

    // Select 作って
    final Select select = new Select("select", new Model<Prefecture>());
    // Renderer 準備するところまでは従来通り
    IOptionRenderer<Prefecture> renderer = new IOptionRenderer<Prefecture>() {
      @Override
      public IModel<Prefecture> getModel(Prefecture value) {
        return Model.of(value);
      }
      @Override
      public String getDisplayValue(Prefecture object) {
        return object.name;
      }
    };
    // 違うのはここだけ
    select.add(new GroupableSelectOptions<Prefecture>("options", prefectures, renderer));
    form.add(select);

    Button submit = new Button("submit"){
      @Override
      public void onSubmit() {
        Prefecture selected = (Prefecture)select.getModelObject();
        if(selected != null) {
          display.setDefaultModelObject(selected.name);
        }
      }
    };
    form.add(submit);
  }
}

OptgroupPage.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  <meta http-equiv="content-script-type" content="text/javascript">
  <meta http-equiv="content-style-type" content="text/css">
  <meta name="description" content="">
  <title>Wicket Test</title>
</head>
<body>
  <h2>GroupableSelectOptions</h2>

  <h4 wicket:id="display">Selected Prefecture Name</h4>

  <form wicket:id="form">
    <select wicket:id="select">
       <!-- 未選択状態を作りたいときは書く。value属性を書かないとException -->
       <option value="">選択してください</option>
       <wicket:container wicket:id="options">
           <option wicket:id="option">Option Label</option>
       </wicket:container>
    </select>
    <input type="submit" value="選択" wicket:id="submit"/>
  </form>
</body>
</html>

需要があるのか、そもそもこれ使いやすいのか疑問はあるのだけれども、とりあえず晒してみる。