TeedaのIDをかぶらないように変更

最近Teedaを使っていて、ある程度使い勝手がわかってきましたが、どうしても嫌いな部分がありました。

idがかぶってしまうこと!

<div id="testItems">
 <div id="title">title_str</div>
 <div id="note">note_str</div>
</div>

こんなテンプレートが

 <div id="title">item0_title</div>
 <div id="note">item0_note</div>

 <div id="title">item1_title</div>
 <div id="note">item1_note</div>

こう出力されます。
本当はJSFなので

 <div id="testItems:0:title">item0_title</div>
 <div id="testItems:0:note">item0_note</div>

 <div id="testItems:1:title">item1_title</div>
 <div id="testItems:1:note">item1_note</div>

こんな感じ? で出力されるのが正しい動きみたいです。そしてTeedaでもJSFの形式で出力するオプションがあります!

ただデフォルトが:区切りじゃないのって、やっぱり使い勝手が悪かったからなんでしょうね。。。
直接的な原因は jQuery でツリーを描画するプラグインがユニークな id でないとうまく動かないことでした。そもそも id がかぶるって xhtml 的にだめですからね。

JSF の仕様は確かに xhtml の仕様として:を使うのは間違っていないのですが、擬似クラス(:hover等)との相性が悪いんですよね。見た目もかっこ悪いし(笑)

つーことで、オレオレ拡張として

 <div id="title-0">item0_title</div>
 <div id="note-0">item0_note</div>

 <div id="title-1">item1_title</div>
 <div id="note-1">item1_note</div>

このように出力されるように改造してみました。当初 ForEach 自体を改造してしまったのですが、あとからJSFの形式にするオプションを教えてもらい、その機能を使って改造しました。

構造

このid名は内部処理と、レンダリング処理で問い合わせて違うものを利用することができます。内部処理は上記の場合「title」などのように要素名と同じものを利用しており、表示時にはJSFなどの形式などに合わせた出力を行っています。

src/main/resources/teedaCustomize.dicon

上記のファイルにて、どのクラスを利用してレンダリング時に id 名を決めるのか登録可能です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<component class="org.seasar.teeda.core.render.DefaultComponentIdLookupStrategy" />
	<component class="org.seasar.teeda.core.application.impl.DefaultComponentLookupStrategy" />
	<component class="org.seasar.teeda.extension.util.SimpleLabelHelperImpl" />
	<component class="org.seasar.teeda.extension.util.SimpleFacesMessageHelperImpl" />
</components>

デフォルトは上記のような内容になっています。ComponentIdLookupStrategy とついている場所がレンダリング時の id 指定クラスになります。

DefaultComponentIdLookupStrategy 標準 name(テンプレート上に設定したid)
JsfSpecComponentIdLookupStrategy JSF形式 form:nameItemsItems:0:nameItems:0:name

設定できるのが上記の2種類です。今回はこのクラスを改造して、作成したクラスを設定することで実装します。

DefaultComponentIdLookupStrategy クラスの内容

public class DefaultComponentIdLookupStrategy implements
        ComponentIdLookupStrategy {

    private boolean cooperateWithForeach;

    public String getId(final FacesContext context, final UIComponent component) {
        if (cooperateWithForeach) {
            if (ForEachContext.getContext().isInForEach()) {
                return component.getClientId(context);
            }
        }
        final String id = component.getId();
        if (id != null) {
            return context.getExternalContext().encodeNamespace(id);
        }
        return component.getClientId(context);
    }

    public void setCooperateWithForeach(final boolean cooperateWithForeach) {
        this.cooperateWithForeach = cooperateWithForeach;
    }
}

上記のような内容になっています。JSF 形式の場合には getClientId をそのまま返して終わりになります。cooperateWithForeach がちょっと不明で何に利用しているかわかりません。。。本体のプログラムには設定しているところがなく、Google で検索してもでてこないので今回は省略してしまいました。本当は残しておいてよいのかもしれません。。。

ただ setCooperateWithForeach って JSF 版にはない関数なので扱いが不明です。

実装

/*
 * Copyright 2004-2008 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.seasar.teeda.core.render;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

/**
 * @author shot
 * @author manhole
 * @author akira
 */
public class PostIndexComponentIdLookupStrategy implements
       ComponentIdLookupStrategy {

   public String getId(final FacesContext context, final UIComponent
component) {
       final String id = component.getId();
       if (id != null) {
           String clientIdList[] = component.getClientId(context).split(":");
           String postIndex = "";
           int i = 0;
           while( i+1 < clientIdList.length ){
               if( clientIdList[i].endsWith("Items") ){
                   // Items
                   postIndex += "-" + clientIdList[i+1];
                   i++;
               }
               i++;
           }

           return context.getExternalContext().encodeNamespace(id) + postIndex;
       }
       return component.getClientId(context);
   }
}

内容はシンプルであまり変わっていません!

主な変更点

String clientIdList[] = component.getClientId(context).split(":");
String postIndex = "";
int i = 0;
while( i+1 < clientIdList.length ){
   if( clientIdList[i].endsWith("Items") ){
       // Items
       postIndex += "-" + clientIdList[i+1];
       i++;
   }
   i++;
}

return context.getExternalContext().encodeNamespace(id) + postIndex;

JSF 型の form:nameItemsItems:0:nameItems:1:name 形式を:で分解

form nameItemsItems 0 nameItems 1 name

上記のデータを最後から1つ前までItemsで終わる要素がないかループします。Items で終わる要素があったら、その要素の次の数値の部分を-をつけて-0のような後ろに付ける文字に変更していきます。

この例の場合

name-0-1

が生成されます。

感想

結構きれいに改造できます。このファイルをプロジェクトに追加して、設定ファイルを変更するだけで対応が可能でした。デザイン重視で JavaScript ガリガリの開発パターンだと id はきれいな方が使い勝手がいいと思います。

S2JDBC-Gen とかもそうですが、どうも人とは違う方向に利用している気がする。。。Teeda はいい意味でかれてきているので、JavaScript と絡む場所で少し気になるところがあるだけで、あとは完成されている気がするんですよね。今後は S2JDBC-Gen を少し研究してみようかな〜。

ただ Teeda のサンプルにある Grid とか Tree とかは jQuery とかで現在実装しているのである程度簡単に組み込めるように整理しようかなっ。どうもサーバー側で元データを改造して出力ってのが好きじゃないですね〜。