# ブロックヘルパー

ブロックヘルパーを使用すると、新しいコンテキストで渡されたブロックを呼び出すことができるカスタムイテレーターやその他の機能を定義できます。

# 基本ブロック

デモンストレーションのために、ヘルパーが存在しないかのようにブロックを呼び出すブロックヘルパーを定義してみましょう。

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{#noop}}{{body}}{{/noop}}
  </div>
</div>

noopヘルパー(「no operation」の略)は、オプションハッシュを受け取ります。このオプションハッシュには、通常のコンパイル済みHandlebarsテンプレートのように動作する関数(options.fn)が含まれています。具体的には、この関数はコンテキストを受け取り、文字列を返します。

Handlebars.registerHelper("noop", function(options) {
  return options.fn(this);
});

Handlebarsは常に現在のコンテキストをthisとしてヘルパーを呼び出すため、thisを使用してブロックを呼び出し、現在のコンテキストでブロックを評価できます。

このように定義されたヘルパーは、コンテキストで定義されたフィールドよりも優先されます。ヘルパーによってマスクされているフィールドにアクセスするには、パス参照を使用できます。上記の例では、コンテキストオブジェクト上のnoopという名前のフィールドは、次のように参照されます。

{{./noop}}

# 基本ブロックのバリエーション

構文をより明確にするために、ラップされたテキストにマークアップを追加する別のブロックヘルパーを定義してみましょう。

<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{#bold}}{{body}}{{/bold}}
  </div>
</div>

boldヘルパーは、テキストを太字にするマークアップを追加します。前と同様に、この関数は入力としてコンテキストを受け取り、文字列を返します。

Handlebars.registerHelper("bold", function(options) {
  return new Handlebars.SafeString('<div class="mybold">' + options.fn(this) + "</div>");
});

# withヘルパー

withヘルパーは、パラメーターをヘルパーに渡す方法を示しています。ヘルパーがパラメーターを指定して呼び出されると、テンプレートから渡されたコンテキストを使用して呼び出されます。

<div class="entry">
  <h1>{{title}}</h1>
  {{#with story}}
    <div class="intro">{{{intro}}}</div>
    <div class="body">{{{body}}}</div>
  {{/with}}
</div>

JSONオブジェクトのセクションに深くネストされたプロパティが含まれていて、親の名前を繰り返したくない場合、このようなヘルパーが役立つ場合があります。上記のテンプレートは、次のようなJSONで役立ちます。

{
  title: "First Post",
  story: {
    intro: "Before the jump",
    body: "After the jump"
  }
}

このようなヘルパーの実装は、noopヘルパーの実装と非常によく似ています。ヘルパーはパラメーターを受け取ることができ、パラメーターは{{mustache}}ブロック内で直接使用される式と同じように評価されます。

Handlebars.registerHelper("with", function(context, options) {
  return options.fn(context);
});

パラメーターは、渡された順序でヘルパーに渡され、その後にオプションハッシュが続きます。

# 単純なイテレーター

ブロックヘルパーの一般的なユースケースは、カスタムイテレーターを定義することです。実際、Handlebarsのすべての組み込みヘルパーは、通常のHandlebarsブロックヘルパーとして定義されています。組み込みのeachヘルパーの動作を見てみましょう。

<div class="entry">
  <h1>{{title}}</h1>
  {{#with story}}
    <div class="intro">{{{intro}}}</div>
    <div class="body">{{{body}}}</div>
  {{/with}}
</div>
<div class="comments">
  {{#each comments}}
    <div class="comment">
      <h2>{{subject}}</h2>
      {{{body}}}
    </div>
  {{/each}}
</div>

この場合、comments配列内の各要素に対して、eachに渡されたブロックを1回呼び出す必要があります。

Handlebars.registerHelper("each", function(context, options) {
  var ret = "";

  for (var i = 0, j = context.length; i < j; i++) {
    ret = ret + options.fn(context[i]);
  }

  return ret;
});

この場合、渡されたパラメーター内のアイテムを反復処理し、各アイテムを使用してブロックを1回呼び出します。反復処理中に、文字列の結果を作成し、それを返します。

このパターンを使用して、より高度なイテレーターを実装できます。たとえば、<ul>ラッパーを作成し、各結果の要素を<li>でラップするイテレーターを作成してみましょう。

{{#list nav}}
  <a href="{{url}}">{{title}}</a>
{{/list}}

このテンプレートは、コンテキストとして次のようなものを使い評価します。

{
  nav: [
    { url: "http://www.yehudakatz.com", title: "Katz Got Your Tongue" },
    { url: "http://www.sproutcore.com/block", title: "SproutCore Blog" }
  ];
}

このヘルパーは元のeachヘルパーに似ています。

Handlebars.registerHelper("list", function(context, options) {
  var ret = "<ul>";

  for (var i = 0, j = context.length; i < j; i++) {
    ret = ret + "<li>" + options.fn(context[i]) + "</li>";
  }

  return ret + "</ul>";
});

underscore.jsやSproutCoreのランタイムライブラリなどのライブラリを使用すると、少し見栄えが良くなります。たとえば、SproutCoreのランタイムライブラリを使用した例を以下に示します。

Handlebars.registerHelper("list", function(context, options) {
  return (
    "<ul>" +
    context
      .map(function(item) {
        return "<li>" + options.fn(item) + "</li>";
      })
      .join("\n") +
    "</ul>"
  );
});

# 条件分岐

ブロックヘルパーのもう1つの一般的なユースケースは、条件文を評価することです。イテレーターと同様に、Handlebarsの組み込みifunless制御構造は、通常のHandlebarsヘルパーとして実装されています。

{{#if isActive}}
  <img src="star.gif" alt="Active">
{{/if}}

制御構造は通常、現在のコンテキストを変更せず、代わりに変数に基づいてブロックを呼び出すかどうかを決定します。

Handlebars.registerHelper("if", function(conditional, options) {
  if (conditional) {
    return options.fn(this);
  }
});

条件式を作成する際には、条件式がfalseと評価された場合にヘルパーが挿入するHTMLブロックをテンプレートで提供できるようにすることがよくあります。Handlebarsはこの問題を、ブロックヘルパーに汎用的なelse機能を提供することで解決します。

{{#if isActive}}
  <img src="star.gif" alt="Active">
{{else}}
  <img src="cry.gif" alt="Inactive">
{{/if}}

Handlebarsは、options.inverseとしてelseフラグメントのブロックを提供します。elseフラグメントの存在を確認する必要はありません。Handlebarsは自動的にそれを検出し、「noop」関数を登録します。

Handlebars.registerHelper("if", function(conditional, options) {
  if (conditional) {
    return options.fn(this);
  } else {
    return options.inverse(this);
  }
});

Handlebarsは、オプションハッシュのプロパティとしてアタッチすることで、ブロックヘルパーに追加のメタデータを提供します。詳細については、読み進めてください。

条件式は、後続のヘルパー呼び出しをelseマスタッシュに含めることで連鎖させることもできます。

{{#if isActive}}
  <img src="star.gif" alt="Active">
{{else if isInactive}}
  <img src="cry.gif" alt="Inactive">
{{/if}}

後続の呼び出しで同じヘルパーを使用する必要はありません。他のヘルパーと同様に、else部分でunlessヘルパーを使用できます。ヘルパーの値が異なる場合は、終了マスタッシュが開始ヘルパー名と一致する必要があります。

# ハッシュ引数

通常のヘルパーと同様に、ブロックヘルパーはオプションのハッシュを最後の引数として受け取ることができます。listヘルパーを再訪し、作成する<ul>要素に任意の数のオプション属性を追加できるようにしてみましょう。

{{#list nav id="nav-bar" class="top"}}
  <a href="{{url}}">{{title}}</a>
{{/list}}

Handlebarsは、最終的なハッシュをoptions.hashとして提供します。これにより、可変数のパラメーターを受け入れると同時に、オプションのハッシュも受け入れることが容易になります。テンプレートがハッシュ引数を提供しない場合、Handlebarsは自動的に空のオブジェクト({})を渡すため、ハッシュ引数の存在を確認する必要はありません。

Handlebars.registerHelper("list", function(context, options) {
  var attrs = Object.keys(options.hash)
    .map(function(key) {
      return key + '="' + options.hash[key] + '"';
    })
    .join(" ");

  return (
    "<ul " +
    attrs +
    ">" +
    context
      .map(function(item) {
        return "<li>" + options.fn(item) + "</li>";
      })
      .join("\n") +
    "</ul>"
  );
});

ハッシュ引数は、位置引数によって発生する複雑さなしに、ブロックヘルパーに多くのオプションのパラメーターを提供するための強力な方法です。

ブロックヘルパーは、プライベート変数を子テンプレートに挿入することもできます。これは、元のコンテキストデータにない追加情報を追加する場合に役立ちます。

たとえば、リストを反復処理するときに、現在のインデックスをプライベート変数として提供できます。

{{#list array}}
  {{@index}}. {{title}}
{{/list}}
Handlebars.registerHelper("list", function(context, options) {
  var out = "<ul>",
    data;

  if (options.data) {
    data = Handlebars.createFrame(options.data);
  }

  for (var i = 0; i < context.length; i++) {
    if (data) {
      data.index = i;
    }

    out += "<li>" + options.fn(context[i], { data: data }) + "</li>";
  }

  out += "</ul>";
  return out;
});

dataオプションを介して提供されるプライベート変数は、すべての子スコープで使用できます。

親スコープで定義されたプライベート変数は、パスのクエリを介してアクセスできます。親イテレーターのindexフィールドにアクセスするには、@../indexを使用できます。

各ヘルパーで新しいデータフレームを作成し、独自のデータを設定してください。そうしないと、ダウンストリームのヘルパーがアップストリームの変数を予期せず変更する可能性があります。

また、既存のデータオブジェクトを操作しようとする前に、dataフィールドが定義されていることを確認してください。プライベート変数の動作は条件付きでコンパイルされるため、一部のテンプレートではこのフィールドが作成されない場合があります。

# ブロックパラメーター

Handlebars 3.0の新機能として、対応するヘルパーから名前付きパラメーターを受け取ることができます。

{{#each users as |user userId|}}
  Id: {{userId}} Name: {{user.name}}
{{/each}}

この特定の例では、userは現在のコンテキストと同じ値を持ち、userIdは反復処理のインデックス値を持ちます。

これにより、ネストされたヘルパーは、プライベート変数で発生する可能性のある名前の競合を回避できます。

{{#each users as |user userId|}}
  {{#each user.book as |book bookId|}}
    User Id: {{userId}} Book Id: {{bookId}}
  {{/each}}
{{/each}}

いくつかの組み込みヘルパーはブロックパラメーターをサポートしており、カスタムヘルパーはblockParamsオプションフィールドを介してそれらを指定できます。

ヘルパーは、options.fn.blockParamsフィールドを介してテンプレートによって参照されるブロックパラメーターの数を判断できます。これは整数カウントです。この値は、子テンプレートで参照できるブロックパラメーターの数を表します。このカウントを超えるパラメーターは参照されることはなく、必要に応じてヘルパーによって安全に省略できます。これはオプションであり、テンプレートに渡される追加のパラメーターは黙って無視されます。

# Rawブロック

未処理のマスタッシュブロックを処理する必要があるテンプレートには、Rawブロックを使用できます。

テンプレート
{{{{raw-loud}}}}
  {{bar}}
{{{{/raw-loud}}}}
{{{{raw-helper}}}}
  {{bar}}
{{{{/raw-helper}}}}

は、コンテンツを解釈せずにraw-helperヘルパーを実行します。

Handlebars.registerHelper("raw-helper", function(options) {
  return options.fn();
});

は、次のようにレンダリングされます。

{{bar}}
最終更新日: 2021年10月20日 午前9時33分06秒