JSF の rendered 属性で id が衝突する場合

JSF のビューテンプレート内で条件分岐してコンポーネントの表示を切り替えたい場合、 rendered 属性で表示するための条件を指定することができます。しかし rendered 属性は、 JSPc:choose などと違い排他構造を表現できないため、 JavaScript で使うためなどでコンポーネントid を指定している場合に重複エラーとなってしまいます。例えば以下のように書いた場合*1

<h:form id="mainForm">
    <h:outputText rendered="#{foo.showText}" id="hogehoge" value="#{foo.value1}" />
    <h:outputText rendered="#{!foo.showText}" id="hogehoge" value="#{foo.value2}" />
</h:form>

rendered に指定している条件は互いに排他なので実際に重複することはありませんが、文書構造上は排他でないので以下のようなエラーになります。

java.lang.IllegalStateException: Component ID mainForm:hogehoge has already been found in the view. 

JSF でも JSTL のタグは使えるので、 c:choose を使うという手もないではない*2ですがなんとも微妙だなーなどと Twitter でつぶやいていたところ、 @den2sn さんにパススルーアトリビュートなるものの存在を教えていただきました。

パススルーアトリビュート( Pass-through attributes )とは

任意の HTML の属性を指定してそのまま出力するための機能です。 JSF 2.2 から使えるようになったようです。使い方は JavaEE 7 HTML5に対応したJSF 2.2 - しんさんの出張所 はてな編 が詳しいです。

パススルーアトリビュートを使うには、 XML 名前空間を追加するだけです。プレフィックスには p を使う例が多いですが、 PrimeFaces とかぶるので pt にしています。

xmlns:pt="http://xmlns.jcp.org/jsf/passthrough"

先ほどの例で id で指定していたのを pt:id に書き換えます。

<h:form id="mainForm">
    <h:outputText rendered="#{foo.showText}" pt:id="hogehoge" value="#{foo.value1}" />
    <h:outputText rendered="#{!foo.showText}" pt:id="hogehoge" value="#{foo.value2}" />
</h:form>

これで HTML 上では id="hogehoge" として無事出力することができます。なお、普通に id を指定した場合とは HTML 上の id が異なる*3ため、 JavaScript で参照している場合は修正が必要です。

そもそも JSF が生成する id を JavaScript から参照するのも非常に微妙な感あるので、そういう場合は基本パススルーアトリビュートを使うのが良いような気もします。

*1:この程度の例であれば value の中で条件分岐して対応することは可能ですが、複雑なコンポーネントで属性値を色々変えたい場合は辛いです

*2:JSFJSTL のタグは処理タイミングが違うので期待した動作にならないことがあるため、あまり使わないほうが良いようです

*3:JSF の id を使うと mainForm:hogehoge のような id になる