ラジオボタン(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)
<h:form> <h:selectOneRadio value="#{genderForm.genderValue}"> <f:selectItems value="#{genderForm.selectItems}"/> </h:selectOneRadio> <p>genderValue</p> <p>#{genderForm.genderValue}</p> <h:commandButton action="#{genderAction.update()}" value="更新"/> </h:form>
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); } }
Enum
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は使わずに、そのままブラウザで表示しても 出力イメージに近い結果になるようにしています。
<h:form> <div style="float: left"> <div> <input type="radio" jsf:id="#{genderForm.targetId(0)}"> <f:passThroughAttributes value="#{genderForm.checked(0)}"/> <f:ajax 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" jsf:id="#{genderForm.targetId(1)}"> <f:passThroughAttributes value="#{genderForm.checked(1)}"/> <f:ajax 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" jsf:id="#{genderForm.targetId(2)}"> <f:passThroughAttributes value="#{genderForm.checked(2)}" /> <f:ajax 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 /> <h:commandButton action="#{genderAction.updateRadio()}" value="更新"/> <br /> <h:commandButton action="#{genderAction.confirm()}" value="確認"/> </h:form>
f:passThroughAttributes
checked
はvalueで値を設定できません。
仕方が無いので、ロジックで動的に出力します。
f:ajax event
ラジオボタンの選択がchecked
でしか表現できないために ボタンをチェックする都度、サーバー側のBeanを更新します。
checked
はvalueで値を設定できないのと同様に、取得もできないためです。
仕方が無いパート2です。。
別にAjax操作の後に画面表記を変える必要は無いのでrender
は不要です。
シンプルな選択(ui:repeat)
繰り返しを使って動的にラジオボタンを出力します。
<div style="float: left" jsfc="ui:repeat" value="#{genderTypeItemsForm.items}" var="genderTypeForm" varStatus="stat"> <div jsf:rendered="#{genderTypeItemsForm.renderChecked(stat.index)}"> <input type="radio" jsf:id="radioOn" pt:name="radioJSF" value="#{genderTypeForm.genderTypeValue}" checked="checked"> <f:ajax event="click" listener="#{genderRepeatAction.change(stat.index)}"/> </input> <label for="#{genderTypeItemsForm.forTargetRadioOn(component)}" > #{genderTypeForm.display} </label> </div> <div jsf:rendered="#{genderTypeItemsForm.renderChecked(stat.index)==false}" class="designOnly"> <input type="radio" jsf:id="radioOff" pt:name="radioJSF" value="#{genderTypeForm.genderTypeValue}"> <f:ajax 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)
<h:form> <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}" pt:name="itemJSF" jsf:id="radioOn" rendered="#{itemsForm.renderChecked(state.index)}"> <f:passThroughAttributes value="#{itemsForm.checked}"/> <f:ajax event="click" listener="#{itemAction.change(state.index)}"/> </input> <input type="radio" value="#{item.itemValue}" pt:name="itemJSF" jsf:id="radioOff" class="designOnly" rendered="#{itemsForm.renderChecked(state.index)==false}"> <f:ajax event="click" listener="#{itemAction.change(state.index)}"/> </input> </td> <td>#{item.itemValue}</td> </tr> </tbody> </table> <p>item = #{itemsForm.checkedItem}</p> <h:commandButton action="#{itemAction.update()}" value="更新"/> </h:form>
「 シンプルな選択(ui:repeat)」の Label が無い版というところです。
その他
id
とLabelのfor
の同値編集については、他にも色々なやり方があると思います。
例えば、idを直接指定するイメージで 以下のような書き方とかもあるでしょう。
<label>性別</label> <div class="field"> <div class="ui radio checkbox"> <input type="radio" jsf:value="MAN" pt:name="gender" jsf:id="gender0"> <f:passThroughAttributes 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" jsf:value="WOMAN" pt:name="gender" jsf:id="gender1"> <f:passThroughAttributes 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" pt:name="gender" jsf:id="gender2"> <f:passThroughAttributes 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; }
参考
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
さいごに
図らずも、JSFのBean参照のタイミングというか、出来る出来ないという範囲が 自分の直感的な理解とは異なるケースがあるというのが分かってよかった気がします。
「理屈としては、ここで参照できるはず」としないで、細かく確認しながら実装することが大事だと改めて思いました。
これはJSFが悪いとか そういう話ではなくて DDDでいうところの Domain以外のところは 実現性可否の調査をちゃんとしましょう、というだけの話だと思っています。