HTMLコンポーネントにHTMLの断片を引数的に渡す

テンプレートエンジンでの部分HTMLはファーストクラスたりえるのかどうか。

例: 会員登録フィームの繰り返し部分を部品化する

単純な登録フォームにも繰り返し部分がある。


例: 新規会員登録フォーム

...
    <form action="/register.confirm" method="post" enctype="multipart/form-data">
      <table class="config">
        <tr>
          <th class="row">ユーザー名<span class="require">(必須)</span></th>
          <td>
            <input value="" name="name" id="username-text" class="text" type="text" />
          </td>
        </tr>
        <tr>
          <th class="row">パスワード<span class="require">(必須)</span></th>
          <td>
            <input value="" name="password" class="password" type="password" />
          </td>
        </tr>
        <tr>
          <th class="row">もう一回<span class="require">(必須)</span></th>
          <td>
            <input value="" name="re_password" class="password" type="password" />
          </td>
        </tr>
        <tr>
          <th class="row">メールアドレス<span class="require">(必須)</span></th>
          <td>
            <input value="" name="mail" class="text" type="text" />
          </td>
        </tr>
      </table>
      <div class="config-button">
        <input value="確認する" class="submit" type="submit" />
      </div>
    </form>

th(項目名)とtd(入力部品)を含むtrが繰り返し現れている。部品化する事で重複を省きたいと思うのだが、中に含まれる項目名や入力部品は項目毎に違うので単純にincludeできない。


Faceletsでは、で外部のHTMLを取り込む際に、タグの内部にで囲った領域を含める事で、その部分を引数のように渡す事が出来る。この機能を利用すれば、共通部分を部品として記述し、部品を呼び出す側には差分のみを書く事で重複を避ける事が出来る。
呼び出す側(register.xhtml):

    <form action="/register.confirm" method="post" enctype="multipart/form-data">
      <table class="config">
        <ui:decorate template="/taglibs/input_row.xhtml">
            <ui:param name="name" value="ユーザー名"/>
            <ui:param name="required" value="true"/>
            <ui:define name="unit">
                <h:inputText value="${regform.username}" name="name" class="text" />
            </ui:define>
        </ui:decorate> 
        <ui:decorate template="/taglibs/input_row.xhtml">
            <ui:param name="name" value="パスワード"/>
            <ui:param name="required" value="true"/>
            <ui:define name="unit">
                <h:inputSecret value="${regform.password}" name="password" class="password" />
            </ui:define>
        </ui:decorate> 
...


部品側(/taglibs/input_row.xhtml):

<?xml version="1.0" encoding="Shift_JIS" ?>
<html xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:c="http://java.sun.com/jstl/core">
<body>
    <ui:composition>
        <tr>
            <th class="row">
                <h:outputText value="${name}" />
                <c:if test="${required}"><span class="require">(必須)</span></c:if>
            </th>
            <td>
                <ui:insert name="unit"/>
            </td>
        </tr>
    </ui:composition>
</body>
</html>


部品側で「」と記述する事で、呼び出し側で「」で定義した領域を表示する事が出来る。
この例はあまり良くないけど、もう少し凝ったパーツを繰り返し表示する場合等は、(表示ON/OFFの処理などを)部品側の一カ所で制御出来たりして有用なこともある。(でも今時はデザイン的な事はスタイルシートだよね。)


渡した領域が呼び出される側で評価されるのが、なんとなくクロージャやブロックぽい感じ。

taglib化

記述量に関してはFaceletsの外部ファイルのインクルードをtaglib化する機能を使えば一応もう少しは減らせる。


/taglibs/editor.taglib.xml:

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://terazzo.dynds.org/facelets/editor</namespace>
    <tag>
        <tag-name>input_row</tag-name>
        <source>input_row.xhtml</source>
    </tag>
</facelet-taglib>

web.xmlに登録

...
     <context-param>
        <param-name>facelets.LIBRARIES</param-name>
        <param-value>/taglibs/editor.taglib.xml</param-value>
    </context-param>
...


呼び出す側(register.xhtml)

<?xml version="1.0" encoding="Shift_JIS" ?>
<html xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:tzo="http://terazzo.dynds.org/facelets/editor">
...
        <tzo:input_row name="ユーザ名" required="true">
            <ui:define name="unit">
                <h:inputText value="${regform.username}" name="name" class="text" />
            </ui:define>
        </tzo:input_row>    
        <tzo:input_row name="パスワード" required="true">
            <ui:define name="unit">
                <h:inputSecret value="${regform.password}" name="password" class="password" />
            </ui:define>
        </tzo:input_row>    
...

あんまり替わらなかった……

再帰呼び出しについて少し実験

不動点関数みたいに再帰呼び出し用のHTML部品を一つ用意しておけば、呼び出す側だけで再帰表示が可能かと思ったが上手く行かない。どうやらDefaultFaceletContextクラス内のTemplateManagerクラスで再帰呼び出しを抑制しているようだ。


ソースコードを修正して制限を外してやるとうまくいく。


com/sun/facelets/impl/DefaultFaceletContext.java(抑制部分をコメントアウトして修正):

...
        public boolean apply(FaceletContext ctx, UIComponent parent, String name)
                throws IOException, FacesException, FaceletException,
                ELException {
            String testName = (name != null) ? name : "facelets._NULL_DEF_";
//            if (this.names.contains(testName)) {	// ←ここで再帰呼び出しを抑制している
//                return false;
//            } else {
                this.names.add(testName);
                boolean found = false;
                found = this.target.apply(new DefaultFaceletContext(
                        (DefaultFaceletContext) ctx, this.owner), parent, name);
                this.names.remove(testName);
                return found;
//            }
        }

再帰用のHTML部品(fix.xhtml):

<?xml version="1.0" encoding="Shift_JIS" ?>
<html xmlns:ui="http://java.sun.com/jsf/facelets">
<body>
    <ui:composition>
        <ui:insert name="func"/>
    </ui:composition>
</body>
</html>

ツリー表示のサンプル。データは前回と同じもの

<?xml version="1.0" encoding="Shift_JIS" ?>
<html xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jstl/core">
<body>
    <h1>Tree</h1>
    <ui:decorate template="fix.xhtml">
        <ui:param name="node" value="${sampleData.rootNode}"/>
        <ui:define name="func">
            ${node.name}
            <c:if test="${node.children != null}">
                <ul>
                <c:forEach items="${node.children}" var="child">
                    <li>
                    <ui:decorate template="fix.xhtml">
                        <ui:param name="node" value="${child}"/>
                    </ui:decorate>
                    </li>
                </c:forEach>
                </ul>
            </c:if>
        </ui:define>
    </ui:decorate>    
</body>
</html>

パラメータiで渡した数に対応するフィボナッチ数の個数のアスタリスクを表示するサンプル

<?xml version="1.0" encoding="Shift_JIS" ?>
<html xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jstl/core">
<body>
    <h1>Fib</h1>

    6:
    <ui:decorate template="fix.xhtml">
        <ui:param name="i" value="${6}"/>
        <ui:define name="func">
            <c:choose>
                <c:when test="${i==0}">
                </c:when>
                <c:when test="${i==1}"></c:when>
                <c:otherwise>
                    <ui:decorate template="fix.xhtml">
                        <ui:param name="i" value="${i-1}"/>
                    </ui:decorate>
                    <ui:decorate template="fix.xhtml">
                        <ui:param name="i" value="${i-2}"/>
                    </ui:decorate>
                </c:otherwise>
             </c:choose>
        </ui:define>
    </ui:decorate>    
</body>
</html>

まあ実用性は皆無か。わざわざ抑制しているのにも理由がありそうなので無くすと危険という気も。