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

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

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