ラジオボタン(SelectOneRadio)を使うことを個人的に あまり使うことがなくて 特に調べていなかったのですが、今 ちょっと やっていることで たまたま出てきて いざJSFで やろうとしたときに 色々と気になったので まとめてみることにしました。
あくまで、私が試した範囲ですが、正直な感想としては「出来そうで出来ないことが結構あるんだなぁ」ということが分かりました。
やりたいこと
シンプルな選択
HTML
目標とする表現のHTMLは以下
<div>
<h2>Pure HTML</h2>
<div style="float: left">
<div>
<input type="radio" value="MAN" name="gender" id="gender0">
</input>
<label for="gender0">男性</label>
</div>
</div>
<div style="float: left">
<div>
<input type="radio" value="WOMAN" name="gender" id="gender1" />
<label for="gender1">女性</label>
</div>
</div>
<div style="float: left">
<div>
<input type="radio" value="OTHER" name="gender" id="gender2"/>
<label for="gender2">不明</label>
</div>
</div>
</div>
JSF(Not HTML Friendly)
<hform>
<hselectOneRadio value="#{genderForm.genderValue}">
<fselectItems value="#{genderForm.selectItems}"/>
</hselectOneRadio>
<p>genderValue</p>
<p>#{genderForm.genderValue}</p>
<hcommandButton action="#{genderAction.update()}" value="更新"/>
</hform>
ActionForm
私はActionとFormを分けて実装する派です。
xhtmlで参照できるように、SelectItemのListへEnumの情報を設定します。
@Named
@SessionScoped
public class GenderForm implements Serializable {
private static final long serialVersionUID = 1L;
private GenderType genderType;
@PostConstruct
public void init() {
this.genderType = GenderType.MAN;
}
public List<SelectItem> getSelectItems() {
List<SelectItem> items = new ArrayList<>();
for (GenderType _genderType : GenderType.values()) {
items.add(new SelectItem(String.valueOf(_genderType.getValue()), _genderType.getDisplay()));
}
return items;
}
public Integer getGenderValue() {
return this.genderType.getValue();
}
public void setGenderValue(Integer genderValue) {
this.genderType = GenderType.createGenderType(genderValue);
}
}
public enum GenderType {
MAN(0, "男性"),
WOMAN(1, "女性"),
OTHER(2, "不明");
private final Integer value;
private final String display;
private GenderType(Integer value, String display) {
this.value = value;
this.display = display;
}
public Integer getValue() {
return value;
}
public String getDisplay() {
return display;
}
public static GenderType createGenderType(Integer value) {
for (GenderType _genderType : GenderType.values()) {
if (_genderType.getValue().equals(value)) {
return _genderType;
}
}
return GenderType.MAN;
}
}
Action
リダイレクトもしないし、自画面遷移なので 何もしない空のアクションだけ。
Injectはコンストラクタインジェクションで。
@Named
@RequestScoped
public class GenderAction implements Serializable {
private static final long serialVersionUID = 1L;
private GenderForm genderForm;
public GenderAction() {
}
@Inject
public GenderAction(GenderForm genderForm) {
this.genderForm = genderForm;
}
public void update() {
}
}
JSF(HTML Friendly)
アクショントリガーとなるボタンについては、今回のメインではないので、JSFのタグで記述しています。
まずは あえて ui:repeatは使わずに、そのままブラウザで表示しても 出力イメージに近い結果になるようにしています。
<hform>
<div style="float: left">
<div>
<input type="radio" jsfid="#{genderForm.targetId(0)}">
<fpassThroughAttributes value="#{genderForm.checked(0)}"/>
<fajax event="click" listener="#{genderAction.change}"/>
</input>
<label for="#{genderForm.targetFor(component.clientId, 0)}">
#{genderForm.targetLabel(0)}
</label>
</div>
</div>
<div style="float: left">
<div>
<input type="radio" jsfid="#{genderForm.targetId(1)}">
<fpassThroughAttributes value="#{genderForm.checked(1)}"/>
<fajax event="click" listener="#{genderAction.change}"/>
</input>
<label for="#{genderForm.targetFor(component.clientId, 1)}">
#{genderForm.targetLabel(1)}
</label>
</div>
</div>
<div style="float: left">
<div>
<input type="radio" jsfid="#{genderForm.targetId(2)}">
<fpassThroughAttributes value="#{genderForm.checked(2)}" />
<fajax event="click" listener="#{genderAction.change}"/>
</input>
<label for="#{genderForm.targetFor(component.clientId, 2)}">
#{genderForm.targetLabel(2)}
</label>
</div>
</div>
<br />
<p>genderValue</p>
<p>#{genderForm.genderValue}</p>
<br />
<hcommandButton action="#{genderAction.updateRadio()}" value="更新"/>
<br />
<hcommandButton action="#{genderAction.confirm()}" value="確認"/>
</hform>
f:passThroughAttributes
checked
はvalueで値を設定できません。
仕方が無いので、ロジックで動的に出力します。
ラジオボタンの選択がchecked
でしか表現できないために ボタンをチェックする都度、サーバー側のBeanを更新します。
checked
はvalueで値を設定できないのと同様に、取得もできないためです。
仕方が無いパート2です。。
別にAjax操作の後に画面表記を変える必要は無いのでrender
は不要です。
シンプルな選択(ui:repeat)
繰り返しを使って動的にラジオボタンを出力します。
<div style="float: left" jsfc="ui:repeat" value="#{genderTypeItemsForm.items}" var="genderTypeForm" varStatus="stat">
<div jsfrendered="#{genderTypeItemsForm.renderChecked(stat.index)}">
<input type="radio" jsfid="radioOn" ptname="radioJSF" value="#{genderTypeForm.genderTypeValue}" checked="checked">
<fajax event="click" listener="#{genderRepeatAction.change(stat.index)}"/>
</input>
<label for="#{genderTypeItemsForm.forTargetRadioOn(component)}" >
#{genderTypeForm.display}
</label>
</div>
<div jsfrendered="#{genderTypeItemsForm.renderChecked(stat.index)==false}" class="designOnly">
<input type="radio" jsfid="radioOff" ptname="radioJSF" value="#{genderTypeForm.genderTypeValue}">
<fajax event="click" listener="#{genderRepeatAction.change(stat.index)}"/>
</input>
<label for="#{genderTypeItemsForm.forTargetRadioOff(component)}" >
#{genderTypeForm.display}
</label>
</div>
</div>
@Named
@SessionScoped
public class GenderTypeItemsForm implements Serializable {
private static final long serialVersionUID = 1L;
private List<GenderTypeForm> items;
private GenderType genderType;
@PostConstruct
public void init() {
List<GenderTypeForm> _items = new ArrayList<>();
_items.add(new GenderTypeForm(GenderType.MAN));
_items.add(new GenderTypeForm(GenderType.WOMAN));
_items.add(new GenderTypeForm(GenderType.OTHER));
this.items = _items;
this.genderType = GenderType.MAN;
}
public List<GenderTypeForm> getItems() {
return items;
}
public void setItems(List<GenderTypeForm> items) {
this.items = items;
}
public Map<String, String> checked() {
Map<String, String> map = new HashMap<>();
map.put("checked", "checked");
return map;
}
public boolean renderChecked(Integer itemIndex) {
return Objects.equals(this.genderType.getValue(), this.items.get(itemIndex).getGenderTypeValue());
}
public String getDisplay() {
return this.genderType.getDisplay();
}
public void updateGenderType(Integer index) {
this.genderType = this.items.get(index).getGenderType();
}
public String forTargetRadioOn(UIComponent component) {
return component.getParent().getClientId() + "-radioOn";
}
public String forTargetRadioOff(UIComponent component) {
return component.getParent().getClientId() + "-radioOff";
}
}
チェックのついたラジオボタンと ついていないラジオボタンを2つ準備しておき、出力するタグを分けます。
先と同じように checked
がvalueで扱えないための苦肉の策です。
Label の for 指定をIDと同じ値にするためのメソッドforTargetRadioOn
と forTargetRadioOff
も 引数を2つ以上指定出来なかったための苦肉の策です。
でも、別の実装お試しをしている時には出来たようなケースもあったので、多分 何か対応が不十分なだけな気もします。
テーブル列の単一行の選択
HTML
目標とする表現のHTMLは以下
<table>
<thead>
<tr>
<th></th>
<th>商品</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="radio" value="AAAA" name="item" id="item0" checked="" /></td>
<td>AA AA</td>
</tr>
<tr>
<td><input type="radio" value="BBBB" name="item" id="item1"/></td>
<td>BB BB</td>
</tr>
<tr>
<td><input type="radio" value="CCCC" name="item" id="item2"/></td>
<td>CC CC</td>
</tr>
</tbody>
</table>
JSF(Not HTML Friendly)
dataTableでの 良いやり方が見つけられませんでした。
pass-through でやろうとしたけど、selectOneRadio配下全てのname属性が全て同じになってしまった htmlコードをみて、たとえ うまく動いたとしても絶対に後々事故の元になると思ったので そっと諦めました。
ということで、実装したいなら input type="radio"
だと思って試しましたが、今度は indexの取得で挫けました。
DataModel型を使えば出来るんでしょうけど、そのための実装量が多すぎるし 分かりにくいし。。
とにかく登場する要素が多すぎて これならdataTableを使わない方が良いや、となりました。
実際、私自身は、JSFを使う際、可能な限り HTML Friendly にするという指針なので dataTable自体を使っていませんでした。なので 諦めてもダメージが少ないというのも本音です。
JSF(HTML Friendly)
<hform>
<table>
<thead>
<tr>
<th></th>
<th>商品</th>
</tr>
</thead>
<tbody>
<tr jsfc="ui:repeat" value="#{itemsForm.items}" var="item" varStatus="state">
<td>
<input type="radio" value="#{item.itemValue}" ptname="itemJSF" jsfid="radioOn"
rendered="#{itemsForm.renderChecked(state.index)}">
<fpassThroughAttributes value="#{itemsForm.checked}"/>
<fajax event="click" listener="#{itemAction.change(state.index)}"/>
</input>
<input type="radio" value="#{item.itemValue}" ptname="itemJSF" jsfid="radioOff" class="designOnly"
rendered="#{itemsForm.renderChecked(state.index)==false}">
<fajax event="click" listener="#{itemAction.change(state.index)}"/>
</input>
</td>
<td>#{item.itemValue}</td>
</tr>
</tbody>
</table>
<p>item = #{itemsForm.checkedItem}</p>
<hcommandButton action="#{itemAction.update()}" value="更新"/>
</hform>
「 シンプルな選択(ui:repeat)」の Label が無い版というところです。
その他
id
とLabelのfor
の同値編集については、他にも色々なやり方があると思います。
例えば、idを直接指定するイメージで 以下のような書き方とかもあるでしょう。
<label>性別</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" jsfvalue="MAN" ptname="gender" jsfid="gender0">
<fpassThroughAttributes value="#{userRegistrationForm.checked(0)}"/>
</input>
<label for="#{userRegistrationForm.targetFor(component,'gender0')}">男性</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" jsfvalue="WOMAN" ptname="gender" jsfid="gender1">
<fpassThroughAttributes value="#{userRegistrationForm.checked(1)}"/>
</input>
<label for="#{userRegistrationForm.targetFor(component,'gender1')}">女性</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="OTHER" ptname="gender" jsfid="gender2">
<fpassThroughAttributes value="#{userRegistrationForm.checked(2)}"/>
</input>
<label for="#{userRegistrationForm.targetFor(component,'gender2')}">その他</label>
</div>
</div>
public String targetFor(UIComponent component, String targetName) {
return component.getClientId() + "-" + targetName;
}
参考
name attribute overriden when specifying input type="radio" as JSF passthrough element - Stack Overflow
JSF - JSF 2.2 selectOneRadioをテーブル一覧に出力させるには(43834)|teratail
「JSF selectOneRadio の同じneme属性のItemをテーブルの縦に割り振りたい。」(1) Java Solution − @IT
How to display the row index in a JSF datatable - Stack Overflow
Jsf datatable get row index program | JSF example code
[ H e p o n ' s T r i c k C u b e J a v a ]
Code
Bitbucket
さいごに
図らずも、JSFのBean参照のタイミングというか、出来る出来ないという範囲が 自分の直感的な理解とは異なるケースがあるというのが分かってよかった気がします。
「理屈としては、ここで参照できるはず」としないで、細かく確認しながら実装することが大事だと改めて思いました。
これはJSFが悪いとか そういう話ではなくて DDDでいうところの Domain以外のところは 実現性可否の調査をちゃんとしましょう、というだけの話だと思っています。