LoginSignup
6
9

More than 5 years have passed since last update.

Thymeleafを拡張して独自タグを実装する(その1)

Last updated at Posted at 2017-10-25

今回はProcessorを自作してThymeleafを拡張する方法をまとめます。
Processorは色々拡張方法があるので、今回は自作のタグを作成する方法をまとめます。
(<th:block>タグみたいなやつです。)

UtilityObjectを自作して拡張する方法は以下を参照してください。
Thymeleafを拡張する(UtilityObject編)

環境

  • Thymeleaf 3.0.8
  • Spring 4.3.5

自作するタグの挙動

TERASOLUNA Frameworkのt:messagesPanelタグを簡素化したものを作成してみます。
詳しい挙動は以下の通りです。

  • タグは<sample:messagesPanel />とする。
  • Modelに"messageList"というkeyで格納されたList<String>の中身を画面に出力する。
  • デフォルトは外側にulタグ、内側にliタグを使って、以下のように出力される。
<ul>
  <li>メッセージ1</li>
  <li>メッセージ2</li>
</ul>
  • タグにouterElement、innerElement属性でタグ名を指定すると、それを使って出力する。
<sample:messagesPanel outerElement="div" innerElement="span" /><div>
  <span>メッセージ1</span>
  <span>メッセージ1</span>
</div>

Processorクラスを作成する

まずは、核となるProcessorクラスを作成していきます。
今回はタグを作りたいので、AbstractElementTagProcessorを継承したクラスを作成します。
(StandardBlockTagProcessorを参考に作成しています。)

まずはコンストラクタ

public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

  public MessagesPanelTagProcessor(String dialectPrefix){
    super(TemplateMode.HTML, dialectPrefix, "messagesPanel", dialectPrefix != null, null, false, 100000);
  }

  // ~省略~

}

親クラスのコンストラクタを呼び出します。引数の意味は以下の通りです。

  • 第1引数はテンプレートモードを指定します。今回はHTMLです。
  • 第2引数はprefixを指定します。Dialectクラスから指定できるようにしています。(今回はsampleにする予定)
  • 第3引数はタグの要素名を指定します。今回はmessagesPanelにします。
  • 第4引数はタグ名にprefixを利用するかどうかを指定します。引数がnullであればfalse、そうでなければtrueにします。(今回はtrueになる予定)
  • 第5引数は属性名を指定します。今回は属性は指定したくないのでnullにします。
  • 第6引数は属性名にprefixを利用するかどうかを指定します。今回は属性を指定していないので、falseにしておきます。
  • 第7引数は優先度を指定します。ここがいまいちわかっていないのですが、とりあえず10000にしてみました。

つづいてdoProcessメソッド

このメソッドで具体的な処理を行います。

public class MessagesPanelTagProcessor extends AbstractElementTagProcessor {

  // ~省略~

  protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) {

    // (1)
    RequestContext requestContext =
                (RequestContext) context
                        .getVariable(SpringContextVariableNames.SPRING_REQUEST_CONTEXT);

    Map<String, Object> model = requestContext.getModel();
    Object messages = model.get("messageList");

    if (messages == null) {
      // (2)
      structureHandler.removeElement();
      return;
    }

    // (3)
    List<String> messageList = (List<String>) model.get("messageList");
    String outerElement =
          tag.getAttributeValue("outerElement") == null ? "ul" : tag
                    .getAttributeValue("outerElement");
    String innerElement =
            tag.getAttributeValue("innerElement") == null ? "li" : tag
                    .getAttributeValue("innerElement");

    // (4)
    IModelFactory modelFactory = context.getModelFactory();
    IModel iModel = modelFactory.createModel();
    iModel.add(modelFactory.createOpenElementTag(outerElement));
    for (String message : messageList) {
        iModel.add(modelFactory.createOpenElementTag(innerElement));
        iModel.add(modelFactory.createText(HtmlEscape.escapeHtml4Xml(message)));
        iModel.add(modelFactory.createCloseElementTag(innerElement));
    }
    iModel.add(modelFactory.createCloseElementTag(outerElement));

    // (5)
    structureHandler.replaceWith(iModel, false);
  }
}
項番 説明
(1) RequestContextを取得することによって、そこからModelを取得することができます。
(2) タグを削除するためには、引数のIElementTagStructureHandlerのremoveElementメソッドを利用します。
(3) タグの属性に指定された値を取得するには引数のIProcessableElementTagのgetAttributeValueメソッドを利用します。
(4) IModelFactoryを利用して要素を作成し、IModelに追加していきます。createOpenElementTagメソッドはいくつか種類があり、属性を指定してタグを作成することもできます。任意のclassなどを指定したい場合は、これを利用することで作成できます。
(5) replaceWithメソッドを用いて、もともと書いてある<sample:messagesPanel />タグをこれまでに作成した要素に置き換えます。

※追記※
どうやらIModelFactory.createTextメソッドは、引数で与えられた文字列にHTMLのタグが含まれる場合、そのまま表示してしまうようです。
Thymeleafのth:textに該当するStandardTextTagProcessorでは、unbescapeというライブラリを用いてエスケープ処理を行っているため、同様のライブラリを用いてエスケープ処理を行うように上記サンプルを修正しましています。

Dialectクラスを作成する

UtilityObjectを自作したときと同様にDialectクラスを作成する必要があります。
すでにDialectクラスを作成している場合はそれに追加する形でもよいです。

Processorを追加した場合はIProcessorDialectインターフェースを実装する必要がありますが、それを実装したAbstractProcessorDialectという抽象クラスがあるので、今回はこれを継承します。

public class SampleDialect extends AbstractProcessorDialect {

    public SampleDialect() {
        super("sample", "sample", 1000);
    }

    @Override
    public Set<IProcessor> getProcessors(String dialectPrefix) {
        Set<IProcessor> processors = new HashSet<>();
        processors.add(new MessagesPanelTagProcessor(dialectPrefix));
        return processors;
    }
}
  • 親コンストラクタの引数は順番に、Dialectの名称、DialectのPrefix、優先度となっています。
    今回はsampleというPrefixを指定しています。
  • getProcessorsメソッドで自作したProcessorクラスをSetで返却します。

Dialectを登録する

こちらもUtilityObjectで実施したのと同様のことを行います。以下を参照してください。
https://qiita.com/d-yosh/items/edf6ac4e19a7f967a058#dialect%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B

動かしてみる

以下のようなControllerとhtmlを作成して動作を確認しました。

@Controller
public class HelloController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(Model model) {

        model.addAttribute("messageList", Arrays.asList("メッセージ1","メッセージ2"));
        return "home";
    }
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
  <h1>Hello world!</h1>
  <hoge:messagesPanel />
  <hoge:messagesPanel outerElement="div" innerElement="span"/>
</body>
</html>

以下のように出力されているはずです。

<html><head>
<meta charset="utf-8">
<title>Home</title>
</head>
<body>
  <h1>Hello world!</h1>
  <ul>
    <li>メッセージ1</li>
    <li>メッセージ2</li>
  </ul>
  <div>
    <span>メッセージ1</span>
    <span>メッセージ2</span>
  </div>
</body>
</html>

ハマったところ

Modelを取得する方法がわからなかった。

とりあえず、デバッグして引数の中身を確認していたらRequestContextを取得できそうだったので、そこから取得するようにしてみました。
もしかしたら、他に良い方法があるのかもしれないです・・・。

setBodyしたら最初のタグが残ったままだった

IElementTagStructureHandlerにsetBodyというメソッドがあるので、これを使えばいいのだろうと思っていました。
そうすると<sample:messagesPanel />というタグも一緒に表示されてしまいました。
調べたところ、removeTagsというメソッドがあるので、これを利用すれば<sample:messagesPanel />が消えるはず、と思っていたら今度はBodyに設定した内容が表示されませんでした。
こいつらの実装クラスを確認してみたところ、おおむねどのメソッドもresetAllButVariablesOrAttributesというメソッドを呼び出していて、こいつが、設定情報を初期状態に戻していました。
最初のタグを生かしたままにしておきたい場合はsetBody、最初のタグを消したいならreplaceWithという使い分けがいいのかなと思います。

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9