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

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

JSFでTableのCheckboxをAjaxで更新

前回は、同一行の情報書き換えをAjaxで行いました。

今回は、明細行の全選択という一覧型の更新ページで良く登場するパターンをAjaxで実装してみたいと思います。

いくつか参考を探してみたのですがrenderの値を表示行ごとに生成するというやり方で実装をしたいと思います。*1

コードは前回との差分のみを掲載しています。

JSFJavaScriptを使うための準備

実装例を記したいところですが、その前にやっておくことがあります。
JSFJavaScriptなどHTML側の人からすると違和感を感じると思われるところがidの扱いです。*2

セパレーター文字列を変更する

JSFで生成されるidはxhtmlで表記している状態と違い、ルートからのパスを構造的に表現しています。

xhtmlで記述したid
id="isselect"

生成されたid
id="f:rep:0:isselect"

セパレーター文字をきちんと意識しておかないとrenderで正しくidを指定する事が出来ません。
デフォルトは:です。
JSFしか使わないのであれば問題はないと思いますが、JavaScriptのライブラリーによっては:だと具合が悪い事があります。今回は-にします。
やり方はweb.xmlに以下を追加するだけです。<param-value>-がセパレータ文字になります。

<context-param>
    <param-name>javax.faces.SEPARATOR_CHAR</param-name>
    <param-value>-</param-value>
</context-param>


変更が反映されたid
id="f-rep-0-isselect"


idのあれこれは、kikutaro777さんの説明が分かりやすいです。*3
JSFで生成されるidあれこれ - Challenge Java EE !

テーブル明細の全選択制御

前置きが長くなりました。
では、具体例を挙げます。

想定ケース

テーブルのタイトルのチェックボックスをONにしたら明細行全てのチェックボックスをONにする。

これだけであればJavaScriptでもできますが、ここでの全選択の要件が表示されていない分も含めて全ての明細だったらどうでしょうか?
例えば、チェックをONにした情報の一覧をcsvにしてダウンロードしたい場合、どうしましょう?

そういうケースを想定した実装例です。

xhtml

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <style type="text/css">
        td, th { border: 1px #000000 solid; }
    </style>
    <h:head>
        <meta charset="UTF-8" />
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <title>JSF SELECT ITEM</title>
    </h:head>
    <body>
        <form jsfc="h:form" id="f">
            <h1>JSF SELECT ITEM PATTERN Table + Ajax All Select</h1>
            <span>
                ページ
                <input type="text" jsfc="h:inputText"  value="#{selectItemTableRowSelect.table.startViewPagePos}">
                    <f:ajax listener="#{selectItemTableRowSelect.changePagePos}" render="f-list"/>
                </input>
                /3
            </span>

            <h:panelGroup id="list">
                <table>
                    <thead>
                        <tr>
                            <th >
                                <input type="checkbox" id="allselect" jsfc="h:selectBooleanCheckbox"  value="#{selectItemTableRowSelect.table.isSelect}">
                                    <f:ajax event="change" listener="#{selectItemTableRowSelect.checkAllSelect}"   render="#{selectItemTableRowSelect.table.allSelectRenderAttrIds}" />
                                </input>
                            </th>
                            <th>SEQ</th>
                            <th>適当な文字列</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr jsfc="ui:repeat" value="#{selectItemTableRowSelect.table.viewForms}" var="row" varStatus="varStatus" id="rep">
                            <td>
                                <input type="checkbox" id="isselect" jsfc="h:selectBooleanCheckbox"  value="#{row.isSelect}" >
                                    <f:ajax event="change" listener="#{selectItemTableRowSelect.checkDetailRowSelect}"   render="f-allselect" />
                                </input>
                            </td>
                            <td>#{row.seq}</td>
                            <td>固定文字ああああ</td>
                        </tr>
                    </tbody>
                </table>
            </h:panelGroup>
            <dir>
                <input type="submit" jsfc="h:commandButton" value="状態取得" action="#{selectItemTableRowSelect.submit()}"/>
            </dir>
        </form>
    </body>
</html>


明細行(1行)Form

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Builder @Data
public class SelectForm {

    private Boolean isSelect;
    private Integer seq;

}


明細行(全明細)Form

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Data
public class SelectForms {

    private static final int MAX_ROW_COUNT = 10;
    private Integer startViewPagePos;
    private String allSelectRenderAttrIds;

    private Boolean isSelect;

    private List<SelectForm> forms;

    private SelectForms() {
    }

    public static SelectForms of(List<SelectForm> forms) {
        SelectForms selectForms = new SelectForms();
        selectForms.setIsSelect(false);
        selectForms.setForms(forms);
        selectForms.replaceAllSelectRenderAttrIds(1);
        return selectForms;
    }

    public List<SelectForm> getViewForms() {
        List<SelectForm> viewForms;
        Integer startIndex = (this.startViewPagePos - 1) * MAX_ROW_COUNT;
        Integer endIndex = startIndex + toMaxIndex(this.startViewPagePos) + 1;
        viewForms = this.forms.subList(startIndex, endIndex);
        return viewForms;
    }

    public void setFormsIsSelect(Boolean isSelect) {
        this.isSelect = isSelect;
        this.forms.stream().forEach(form -> {
            form.setIsSelect(isSelect);
        });
    }

    public void setFormIsSelect(JsfAjaxBehaviorEvent jsfEvent) {
        this.isSelect = false;
        this.getForms().get(jsfEvent.uiRepeatRowIndex()).setIsSelect((Boolean) jsfEvent.getValue());
    }

    public void replaceAllSelectRenderAttrIds(Integer pagePos) {
        this.startViewPagePos = pagePos;
        this.allSelectRenderAttrIds = JsfContextUtil.editRepaetTargetRender("isselect", this.toMaxIndex(pagePos), "f", "rep");
    }

    private Integer toMaxIndex(Integer pagePos) {
        Integer maxPageCnt = this.forms.size() / MAX_ROW_COUNT;
        Integer rowCountMod = this.forms.size() % MAX_ROW_COUNT;
        maxPageCnt = rowCountMod < (MAX_ROW_COUNT - 1) ? maxPageCnt + 1 : maxPageCnt;
        return Objects.equals(pagePos, maxPageCnt) ? rowCountMod - 1 : MAX_ROW_COUNT - 1;
    }

}


Controller(ManagedBean)

package com.mycompany.samplejsf.domain.selectitem.selectform;

@Named(value = "selectItemTableRowSelect")
@SessionScoped
@NoArgsConstructor
public class SelectItemTableRowSelectController implements Serializable {

    private static final long serialVersionUID = 1L;

    @Getter @Setter
    private SelectForms table;

    @PostConstruct
    public void init() {
        List<SelectForm> forms = new ArrayList<>();
        for (int i = 0; i < 28; i++) {
            SelectForm form = SelectForm.builder().isSelect(false).seq(i + 1).build();
            forms.add(form);
        }

        this.table = SelectForms.of(forms);
        this.printLog("init");
    }

    public void submit() {
        this.printLog("submit");
    }

    public void checkAllSelect(AjaxBehaviorEvent event) {
        this.table.setFormsIsSelect((Boolean) JsfAjaxBehaviorEvent.of(event).getValue());
        this.printLog("ajax all select");
    }

    public void changePagePos(AjaxBehaviorEvent event) {
        this.table.replaceAllSelectRenderAttrIds((Integer) JsfAjaxBehaviorEvent.of(event).getValue());
        this.printLog("ajax change page position");
    }

    public void checkDetailRowSelect(AjaxBehaviorEvent event) {
        this.table.setFormIsSelect(JsfAjaxBehaviorEvent.of(event));
        this.printLog("ajax " + JsfAjaxBehaviorEvent.of(event).getValue());
    }

    private void printLog(String label) {
        System.out.println("label = " + label + "::allSelect:" + table.getIsSelect());
        this.getTable().getForms().stream()
                .forEachOrdered(form -> {
                    System.out.println(":rowSelect:" + form.getIsSelect() + ":seq:" + form.getSeq());
                });
    }
}


部品クラス

package com.mycompany.samplejsf.infrastructure.util.jsf;

import javax.faces.context.FacesContext;

public class JsfContextUtil {

    private JsfContextUtil() {
    }

    public static String getNamingContainerSeparatorChar() {
        return String.valueOf(FacesContext.getCurrentInstance().getNamingContainerSeparatorChar());
    }

    public static String editRepaetTargetRender(String targetAttrId, Integer maxIndex, String... prefixAttrsIds) {
        Integer prefixAttrsLength = prefixAttrsIds.length;
        String separator = JsfContextUtil.getNamingContainerSeparatorChar();
        String[] renderAttrIds = new String[maxIndex + 1];
        for (int i = 0; i <= maxIndex; i++) {
            String[] render = new String[prefixAttrsLength + 2];
            System.arraycopy(prefixAttrsIds, 0, render, 0, prefixAttrsLength);
            render[prefixAttrsLength] = String.valueOf(i);
            render[prefixAttrsLength + 1] = targetAttrId;
            String renderAttr = String.join(separator, render);
            renderAttrIds[i] = renderAttr;
        }
        return String.join(" ", renderAttrIds);
    }

}


実行結果

全明細チェック前

f:id:vermeer-1977:20161222140147p:plain


全明細チェック後

f:id:vermeer-1977:20161222140146p:plain
全明細にチェックが入ります

ページの値を変更

f:id:vermeer-1977:20161222140145p:plain


明細の表示が切り替わります。そしてチェックボックスは意図通りONになっています。


実際のシステムではもっと検証ロジックなど必要だと思いますが、表示切替の事例という事で手を抜いています。*4

ポイント

ポイントはrenderの指定です。
1つ目は、全明細チェック部分の
render="#{selectItemTableRowSelect.table.allSelectRenderAttrIds}"です。
Ajaxのeventをトリガーにして明細行分のrenderの文字列を編集して動的に更新対象を指定しています。

2つ目は、明細行側のチェック部分のrender="f-allselect"です。
生成された後のルートからの階層化されたidを指定してください。
セパレーター-は上述で指定した値です。

3つ目は、明細行のページ切り替え時の更新部分のrender="f-list"<h:panelGroup id="list">です。
xhtmltableタグにidを付与しても、JSF側では認識してくれません。<h:panelGroup>で更新対象をグループにまとめてidを付与してください。そうするとJSFが認識できるidを持った<span>タグが生成されます。

なお前回のような同じ行のAjax操作におけるrenderはidだけの指定で問題ありません。良い感じに接頭文字を編集してくれています。

ちょっとした事

idについては具体的に起動後のソースを確認をするのが一番分かりやすいです。
ちなみに、AjaxJavaScriptも使わないのであれば、Web系エンジニアやデザイナーの人たちには怒られるかもしれませんがxhtmlidを指定しなくても問題はありません。*5

さいごに

なお、画面表示しているものだけを対象とした全選択であれば、Ajaxを使うことなくJavaScriptで沢山のTipsがありますので、そちらを採用した方が楽です。状態の反映はsubmitしたときに全体として反映されるので問題も起きません。

Ajaxでの更新対象の指定ですが@formもあります。これを使えば今回のような細かい実装は不要です。そういう意味では便利です。ただform全体を通信することは忘れないでください。レスポンスが悪化する可能性があります。

*1:UIComponentを直接更新するというような方法もあるようですが、私の技術不足&UIComponentを直接更新するやり方はデザインの所在が分かりにくくなるような気がするので避けたい、という考えから、今回のやり方に私は落ち着きました

*2:ちなみに私はハマりました

*3:私がJSFの調べ物をすると良くたどり着くブログです。いつもお世話になっています。

*4:表示ページ値のValidationをすべきですがやっていません。また表示行数取得部分も最低限の処理しかしていません。例えば保持データ量が0件のケースなど考慮していません

*5:今回の事例でも手抜きが散見されます。問題が無い事と正しい事は違いますので、そこは適宜対応してください。