システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

JSFのラジオボタン

ラジオボタン(SelectOneRadio)を使うことを個人的に あまり使うことがなくて 特に調べていなかったのですが、今 ちょっと やっていることで たまたま出てきて いざJSFで やろうとしたときに 色々と気になったので まとめてみることにしました。

あくまで、私が試した範囲ですが、正直な感想としては「出来そうで出来ないことが結構あるんだなぁ」ということが分かりました。

やりたいこと

  • JSFラジオボタンを使った画面操作をしたい

  • JSFのタグと、HTML Friendlyの両パターンの実装を比べたい

  • シンプルな選択

  • テーブル列の単一行選択

シンプルな選択

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

checkedvalueで値を設定できません。

仕方が無いので、ロジックで動的に出力します。

f:ajax event

ラジオボタンの選択がcheckedでしか表現できないために ボタンをチェックする都度、サーバー側のBeanを更新します。

checkedvalueで値を設定できないのと同様に、取得もできないためです。

仕方が無いパート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つ準備しておき、出力するタグを分けます。

先と同じように checkedvalueで扱えないための苦肉の策です。

Label の for 指定をIDと同じ値にするためのメソッドforTargetRadioOnforTargetRadioOff も 引数を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;
}

参考

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以外のところは 実現性可否の調査をちゃんとしましょう、というだけの話だと思っています。