読者です 読者をやめる 読者になる 読者になる

プログラミングメモ

ソフトウェア開発に関する技術メモ。

動的に追加・削除できる入力欄をJSFで作成する

同じ内容を2個以上入力できるフォームで、ボタンクリックにより入力欄の追加や削除をできるようにしたい。Googleで検索してみるとui:repeatを使った解が見つかるが、実際試してみると、場合によっては不自然な挙動になり、実際に使うのには具合が悪かった。色々試した結果、PrimeFacesのp:dataListを使って実現できた。

 

確認環境
作成する画面例

f:id:teematsu:20150403223014j:plain

  •  氏名とe-mailアドレスをペアで入力する
  • このペアの単位で、入力欄を追加・削除できる
Backing Bean
  • 氏名, e-mailをプロパティとして持つMemberクラスを定義し
  • Backing Beanには、List<Member> を持たせる。

Memberクラス

deleteについては後述。

Backing Bean (RegisterPageクラス)

プロパティmembersがList<Member>となっている。

初期表示時に1個だけ入力欄が表示されるように、initメソッドでMemberを1個だけmembersに追加。 

actionのメソッドは後述。

Facelets

p:dataListのコンテントが、氏名・e-mailの入力欄ペアとなっており、これがp:dataListによって繰り返される。

p:dataListはPrimeFacesが提供するタグで、列数1に限定したh:dataTableのようなもの。h:dataTableはHTMLのtable/tr/tdタグを出力するが、p:dataListはul/li、ol/li、dl/ddを出力する。ただし、type="none"を指定することでul/liなどは出力されなくなるため、自分で(ほぼ)自由にHTMLを組み立てられ、ui:repeatに近い使い方もできる。

valueアトリビュート、varアトリビュートはui:repeatと同じで、それぞれ、参照するリストと、リストの要素を代入する変数名を指定する。#{registerPage.members} でList<Member>を参照し、その要素(この例ではMemberクラス)が member に代入されてp:dataListのコンテントが処理される。

p:dataListのコンテントでは、Memberクラスのname, emailプロパティそれぞれをh:inputTextに結びつけて入力できるようにしている。

以上で、List<Member>の要素数と同じだけ入力欄が表示されるようになる。

入力欄の追加

「入力欄を追加する」ボタンをクリックすればRegisterPage#addMember()メソッドによりList<Member>に要素が追加され、p:dataListで表示される入力欄も1個増える。

入力欄追加のh:commandButtonにはimmediate="true"を指定している。これはvalidationをスキップするためだ。Memberクラスのnameプロパティにはvalidationのルール(@NotNull @Size(min = 1, max = 10))を指定してあるが、「入力欄を追加」時にはvalidationをせずにアクションを実行したい。immediate="true"を指定することでvalidationをする前にアクションを呼出せる。

入力欄の削除

追加の逆で、List<Member>から要素を削除すれば良いように思われるし、「削除」ボタンにimmediate="true"を付けない場合はそれでうまく行った。しかし、immediate="true"を付けると、どの「削除」を押しても、最後の入力欄が削除されるという動作になった。

この問題を回避するために、List<Member>から要素を削除するのはあきらめ、代わりにMemberにdeleteプロパティを持たせ、これがtrueなら画面には表示しない、という方法を取った。delete=trueなら表示されないようにするため、p:dataListのコンテントは<ui:fragment rendered="#{not member.deleted}"> で囲んだ。

実用にあたって
  • p:dataListは、内容全体を2つのdivタグで囲み、borderを表示する。このborderを消したい場合は、出力されたHTMLを見ながら、適切にCSSクラスとスタイルを追加する。上記の例では、p:dataList の styleClass="repeated-form" と、h:headタグ内のスタイル定義が該当する。divタグ自体は消せない。全体を<ul>などで囲みたい場合にはdivが間に入るとHTML文法上正しくないので困る。この場合はp:dataListにulやolを出力させるようにtype= を設定して回避する。<table>, <tr>などで囲みたい場合も困るが、この場合はtableを出力するp:dataTableに置き換えると良いかもしれない(未検証)。なお、h:dataTableはimmediate="true"指定時に入力値が維持されない問題がありNG。
  • また、PrimeFacesはその表示にjQuery UI由来のスタイルを適用するため、PrimeFacesベースで画面を作っていない場合はp:dataListの中だけフォントサイズやボタンのスタイルが変わってしまう。これは、冒頭に示した表示例でも顕著だ。これに対処する正式な方法は、jQuery UIの「テーマ」を作成し、PrimeFacesのマニュアルに従い、組み込むことであるが、一筋縄ではいかない。ここもHTMLを観察しながらCSSクラスとスタイルでがんばるのが適当かもしれない。
  • 追加・削除ボタンに <f:ajax ... /> を追加してAJAXによる部分再描画で画面を更新するようにしても、問題なく動作する。f:ajaxのrender アトリビュート(再描画対象)には、p:dataListのidか、さらにその外側のものを指定する。
  • 氏名・email欄のvalueは、上記の例では #{member.name} などとしているが、代わりに、p:dataListのvarStatusと組み合わせて、#{registerPage.members[memberStatus.index].name} というように指定しても、理屈上は問題なさそうだし、Googleで検索して見つかるui:repeatの例では、むしろ後者しか動作しないと説明しているものもある。しかしp:dataListの場合は、後者は動作しなかった。NullPointerExceptionが発生したりする。p:dataListのvarStatusの動作が不完全のような印象を受けた。
  • List<Member>から要素を削除すると画面に正しく反映されない現象は、h:inputText(UIInput)にsubmitted valueが残っている場合に発生するのではないかという気がする。submitted valueが残っていれば、Backing Beanの値ではなく、残っている submitted valueが表示される。h:commandButtonにimmediate="true"を指定すれば、Update Model Valuesフェーズはスキップされるからsubmitted valueが残ったままになる。submitted valueをクリアする手段があれば解決するかもしれない。ただし、List<Member>の、目的のn番目に対応するsubmitted valueだけピンポイントで消す必要がある。
  • 今回の例の追加ボタンは、List<Member>の最後に要素を追加した。要件によっては途中に挿入したいケースもあるかもしれない。試していないが、削除が思ったような動作にならかったのと同じ理由で、途中へ挿入した場合も正しく表示できない気がする。