まず1番最初は、最も泥臭い方法です。forループ後だと変数から期待する値を取得できないので、forループ内で事を済ませてしまう、という発想です。コードは次のようになります。
{%- for post in posts -%} {%- if loop.first -%} {# 変数の初期化 #} {%- set post_names = '' -%} {%- endif -%} {%- set post_names = post_names + post.name + ',' -%} {%- if loop.last -%} {# macroに文字列をリストとして渡す #} {{ post_list({ items: post_names|split(',') }) }} {%- endif -%} {%- endfor -%}
まず1つ目のループ(loop.first)で変数の初期化を行います。リストに対する追加メソッドがないので、結局文字列型としています。ループ毎にポスト名(post.name)をpost_names変数に追加していき、最後のループ(loop.last)でmacroに変数を渡します。loop.firstとloop.lastはforループ内に予め用意されている予約変数です(参考:For loops | HubL Reference)。
このとき変数に格納されている値は文字列型ですので、macroに渡すときにsplitフィルタを使用してリストに変換しています。これで見事、期待する形でデータが表示されます。
しかしまぁ、いかんせん泥臭いのと、ループがネストされると可読性がどんどん悪くなります。
次に紹介するのは、HubSpotのTextモジュールを使用してforループ内から擬似的にグローバル変数を作成する方法です。とはいってもグローバルに定義しますので、乱用は禁物です。ほぼ裏技みたいなものですね……。コードは次のようになります。
{%- for post in posts -%} {%- if loop.first -%} {# 変数の初期化 #} {%- set post_names_in_loop = '' -%} {%- endif -%} {%- set post_names_in_loop = post_names_in_loop + post.name + ',' -%} {%- if loop.last -%} {# Textモジュールの設置、値の設定 #} {%- text "post_names" , value="{{ post_names_in_loop }}", export_to_template_context=True, overrideable=False -%} {%- endif -%} {%- endfor -%} {# macroに文字列をリストとして渡す #} {{ post_list({ items: widget_data.post_names.value|split(',') }) }}
まず本筋ではありませんが、変数名が紛らわしいためforループ内でのみ使用するpost_namesは「post_names_in_loop」という名前に変更しました。
loop.lastでmacroを呼び出すのではなく、Textモジュールを設置します。valueのダブルクォーテーションの中で変数展開することによってpost_names_in_loopの文字列(「ポスト名1,ポスト名2,ポスト名3,」)をTextモジュールに設定できます。Textモジュールは値のみを使用するため、export_to_template_contextをTrueに設定します。またこのままだとページ編集画面などでTextモジュールの編集が可能な状態ですので、overrideableをFalseに設定します。
これでグローバル変数のように、どこからでも値にアクセスできるようになりました。後はforループの外でmacroを呼び出すと、1つ目の方法と同じように期待通りに各ポスト名が出力されます。
最後に紹介するのは、appendメソッドを使用するです。「pushのようなメソッドは無いのでは?」と思われるかもしれませんが、それはそれでその通りです。HubLやJinja2に配列の末尾に要素を追加するメソッドはありません。このappendメソッドはPythonのメソッドです。そのため、これもまた裏技というか、かなり非公式な方法になります……。が、この方法が1番シンプルで綺麗です。
コードは次のようになります。
{# 変数の初期化 #} {%- set post_names = [] -%} {%- for post in posts -%} {{ post_names.append(post.name)|cut(true) }} {%- endfor -%} {# macroにリストを渡す #} {{ post_list({ items: post_names }) }}
かなりスッキリしましたね。変数も、最初から文字列型ではなくリストで初期化しています。そのためmacroに渡すときも、いちいちsplitフィルタを使う必要がありません。注意点として、まずappendメソッドを使用するには式デリミタ(波括弧2つ)内でなければなりません。
次の注意点として、このスタイルでappendメソッドを使用すると、返り値としてBooleanのtrueを返します。そのため、何もしないと「true」という文字が出力されていまいます。
appendメソッドの返り値のtrueが表示されているこの「true」を出力しないためにcutフィルタを使用してtrueを削除しています。文字列のためのcutフィルタをBooleanに使っていいのかって感じですが……。とにかく、これで無駄なものは表示されず、かつコードもかなりスッキリしました。HubLドキュメントにもJinja2ドキュメントにも書いていない非公式な方法ではありますが、Pythonのメソッドであればいきなり使えなくなることもないだろうと踏んで、現在私は主にこの方法を使用しています。
データの整形は今まで紹介した形で概ねできるようになりましたので、ここからは実際の活用例を紹介します。
Choiceモジュールとは、ページ編集画面でこのUIを提供するモジュールですね。
Choiceモジュール通常通り使用するには、次のように「choices」属性にカンマ区切りで値を設定することにより、それぞれが選択肢として認識されます。
{%- choice "selected_post" label='ポストの選択', value='', choices='選択肢1,選択肢2,選択肢3' -%}
先ほど整形したデータをChoiceモジュールに渡すには、次のようにchoices属性の中で式デリミタを使用します。
{%- choice "selected_post" label='ポストの選択', value='', choices='{{ post_names }}' -%}変数が展開され、Choiceモジュールに適用された
なおChoiceモジュールがchoicesの値として受け付けるのは、カンマ区切りの文字列型です。appnedメソッド以外でデータを整形する方法は最初から文字列型なのでよいですが、appendメソッドを使用してデータを用意した場合は、Choiceモジュールに渡す前にjoinフィルタを使用して文字列型に変換しておく必要があります(choicesのクォーテーション内でjoinフィルタを使用しても、上手くいきません)。
リストをChoiceモジュールに設定する場合{# joinフィルタを使用して、予め文字列型にしておく #} {%- set post_names = post_names|join(',') -%} {%- choice "selected_post" label='ポストの選択', value='', choices='{{ post_names }}' -%}
よくブログポスト画面で、同じタグが付いているポストを「関連記事」として表示したいことがあると思います。1番手っ取り早いベストプラクティスはBlog related postsタグを使用することですが、macroを多段に使用していたりなど、ときに使用しづらいこともあると思います。
そんなときはblog_recent_tag_posts関数を使用して同一のタグを含むポストを取得する訳ですが、普通にやると現在閲覧中の記事まで関連記事に出てしまいます。そんなときに、appendメソッドを使用して現在閲覧中の記事を除外するリストを作成することが可能です。コードは次の通りです。
{# 同じタグが付いたポストの取得 #} {%- set relative_posts = blog_recent_tag_posts('default', content.topic_list[0].slug, 4) -%} {# リストとカウンターの初期化 #} {%- set posts = [] -%} {%- set posts_count = 0 -%} {%- for item in relative_posts -%} {# 現在閲覧中の記事ではなく、ポストが3件に達していなければリストに追加 #} {%- if item.name != content.name|striptags and posts_count < 3 -%} {{ posts.append(item)|cut(true) }} {%- set posts_count = posts_count + 1 -%} {%- endif -%} {%- endfor -%}
ちょっと複雑になってしまいましたね。1つずつ解説します。
これでposts変数には、現在閲覧中の記事を除外した3件の記事のディクショナリがリストとして格納されています。
以上、HubLでリストを扱う際の方法と、活用例をご紹介しました。解決方法については、結局どれも若干モヤっとするところはありますが、テンプレートエンジンなので表現力に乏しいのは仕方ないですね。HubSpot CMSのテンプレートを開発するにおいてリストは避けて通れませんので、何かの際に、ご参考になれば幸いです。