は単体のセレクトボックスの更新でした。
今回はテーブル構造のリストの一部を更新したケースです。 Ajaxでは繰り返し行の一部だけを更新しています。*1
今回はControllerに変換に使用するクラスを保持するというやり方をしていますが、実際の開発ではServiceを経由してリストのデータを取得するというやり方もあると思います。
その場合は、初期表示時のリストについては、Service側もしくはDTOからFormに変換する際に設定しておくというやり方になると思います。
明細行(1行)Form
package com.mycompany.samplejsf.domain.selectitem; @Builder @Data public class ItemForm { private Integer seq; private Integer genderCode; private JsfSelectItem genderEnum; private Integer snackCode; private JsfSelectItem snackList; }
明細行(全明細)Form
package com.mycompany.samplejsf.domain.selectitem; @Builder @Data public class ItemForms { @Singular private List<ItemForm> forms; public ItemForm get(int rowIndex) { return this.forms.get(rowIndex); } }
Builderパターンの実装も@Builder(lombok)で簡単にできます。またlombokの@Builder+@SingularはListへの初期化メソッドに単数と複数の両方を自動生成してくれるので便利なのでお勧めです。
ManagedBean
package com.mycompany.samplejsf.domain.selectitem; @Named(value = "selectItemTableAjax") @SessionScoped @NoArgsConstructor public class SelectItemTableController implements Serializable { private static final long serialVersionUID = 1L; @Getter @Setter private ItemForms genderSnackTable; private Map<Gender, Map<Integer, String>> genderShackMap; @PostConstruct public void init() { Map<Gender, Map<Integer, String>> enumMap = new EnumMap<>(Gender.class); Map<Integer, String> maleSnack = this.maleSnack(); enumMap.put(Gender.MALE, maleSnack); Map<Integer, String> femaleSnack = this.femaleSnack(); enumMap.put(Gender.FEMALE, femaleSnack); this.genderShackMap = enumMap; Integer defaultMaleSnackCode = this.genderShackMap.get(Gender.MALE).entrySet().iterator().next().getKey(); Integer defaultFemaleSnackCode = this.genderShackMap.get(Gender.FEMALE).entrySet().iterator().next().getKey(); this.genderSnackTable = ItemForms.builder() .form(ItemForm.builder() .seq(1) .genderCode(Gender.MALE.getCode()) .genderEnum(JsfSelectItem.of(Gender.class)) .snackCode(defaultMaleSnackCode) .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.MALE))) .build()) .form(ItemForm.builder() .seq(2) .genderCode(Gender.FEMALE.getCode()) .genderEnum(JsfSelectItem.of(Gender.class)) .snackCode(defaultFemaleSnackCode) .snackList(JsfSelectItem.of(this.genderShackMap.get(Gender.FEMALE))) .build()) .build(); this.printLog("init"); } public void submit() { this.printLog("submit"); } public void changeRowGender(AjaxBehaviorEvent event) { int index = JsfAjaxBehaviorEvent.of(event).uiRepeatRowIndex(); ItemForm form = genderSnackTable.get(index); Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode()); form.setSnackCode(this.genderShackMap.get(gender).entrySet().iterator().next().getKey()); form.setSnackList(JsfSelectItem.of(this.genderShackMap.get(gender))); this.printLog("ajax " + index); } private Map<Integer, String> maleSnack() { Map<Integer, String> map = new LinkedHashMap<>(); map.put(1, "大福"); map.put(2, "おはぎ"); map.put(3, "みたらしだんご"); map.put(4, "せんべい"); return map; } private Map<Integer, String> femaleSnack() { Map<Integer, String> map = new LinkedHashMap<>(); map.put(5, "チョコ"); map.put(6, "クッキー"); map.put(7, "プリン"); map.put(8, "ゼリー"); return map; } private void printLog(String label) { System.out.println("label = " + label); this.getGenderSnackTable().getForms().stream() .forEachOrdered(form -> { Gender gender = EnumCodeProperty.codeOf(Gender.class, form.getGenderCode()); String snackName = this.genderShackMap.get(gender).get(form.getSnackCode()); System.out.println("SEQ:" + form.getSeq() + ":GenderCode:" + form.getGenderCode() + ":GenderValue:" + gender.getProperty() + ":SnackCode:" + form.getSnackCode() + ":SnackName:" + snackName); }); } }
JSFの選択行情報を取得する部品クラス
package com.mycompany.samplejsf.infrastructure.part.jsf; @RequiredArgsConstructor(staticName = "of") @EqualsAndHashCode public class JsfAjaxBehaviorEvent { private final AjaxBehaviorEvent event; public int uiRepeatRowIndex() { String clientId = this.event.getComponent().getParent().getClientId(); Pattern pat = Pattern.compile("[0-9]*$"); Matcher mat = pat.matcher(clientId); if (mat.find() == false) { throw new IllegalArgumentException("this method must call ui:repeat ajax event"); } return Integer.parseInt(mat.group()); } }
過去記事の同名クラスへの差分のみ記載しています。
this.event.getComponent().getParent()
がindex
を取得できれば良かったのですが、私には出来ませんでした。。正規表現は苦肉の策です。
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"> <h1>JSF SELECT ITEM PATTERN Table + Ajax</h1> <table> <thead> <tr> <th>SEQ</th> <th>性別</th> <th>お菓子</th> </tr> </thead> <tbody> <tr jsfc="ui:repeat" value="#{selectItemTableAjax.genderSnackTable.forms}" var="row" varStatus="varStatus" id="rep"> <td> #{row.seq} </td> <td> <select jsfc="h:selectOneMenu" value="#{row.genderCode}"> <option jsfc="f:selectItems" value="#{row.genderEnum.values}" /> <f:ajax listener="#{selectItemTableAjax.changeRowGender}" render="snack" event="change" /> </select> </td> <td> <select id="snack" jsfc="h:selectOneMenu" value="#{row.snackCode}"> <option jsfc="f:selectItems" value="#{row.snackList.values}" /> </select> </td> </tr> </tbody> </table> <dir> <input type="submit" jsfc="h:commandButton" value="状態取得" action="#{selectItemTableAjax.submit()}"/> </dir> </form> </body> </html>
デザインをHtmlという前提の場合、繰り返し部分は ui:repeat を使います。
(CSSは手を抜いています。)
実行結果
操作前
操作後
コンソールログ
label = init SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福 SEQ:2:GenderCode:2:GenderValue:gender_female:SnackCode:5:SnackName:チョコ label = ajax 1 SEQ:1:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福 SEQ:2:GenderCode:1:GenderValue:gender_male:SnackCode:1:SnackName:大福
Code
2018/4/17 追加
さいごに
表題とは関係ありませんが、明細全体をファーストクラスコレクションにしておくことで明細行全体としての関心事を明確に凝集させています。
今回は明細全体での振舞いは実装していませんが、全選択時の振舞いなど実際の開発ではあると思います。
あとから凝集させることももちろんできますが、事前にファーストクラスコレクションとしてまとめておく癖をつけておけばリファクタリング時に「関心事」の塊が明確なのでControllerにロジックが漏れにくいと思います。
プロジェクトによっては凝集のために新たにクラスを作成しようとすると、それだけでも仰々しいと思われて抵抗されるケースがあります。
始めからクラスになっていれば、そういうことにはなりにくいです。*2
意味があるのかな?何を実装するのかな?このくらいなら別に良いだろう、と思われる気がするファーストクラスコレクションですが、地味に効いてくる作法だと思います。