クライアントへの理解

JsRender の高度なテンプレート機能

John Papa

コード サンプルのダウンロード

John Papaテンプレートは強力ですが、テンプレート エンジンが既定で提供する標準機能以上の機能が必要になることがあります。データ変換、カスタム ヘルパー関数の定義、独自タグの作成などが必要になります。さいわい、JsRender の中核機能を使用して、このような作業をはじめ、さまざまなことを実行できます。

4 月のコラム (msdn.microsoft.com/magazine/hh882454、英語) では、JsRender テンプレート ライブラリの基本機能を取り上げました。今回は、引き続き、外部テンプレートのレンダリング、{{for}} タグによるコンテキストの変更、複合式の使用など、シナリオを変えて JsRender の機能を見ていきます。また、カスタム タグ、コンバーター、コンテキスト ヘルパーの作成、カスタム コードの許可など、JsRender のより強力な機能をいくつか、実際の使い方を交えて紹介します。すべてのコード サンプルは、archive.msdn.microsoft.com/mag201205ClientInsight (英語) からダウンロードできます。JsRender は、bit.ly/ywSoNu (英語) からダウンロードできます。

{{for}} のバリエーション

{{for}} タグが理想的なソリューションになり得る方法がいくつかあります。前回は、ブロックを使用した配列の反復処理に {{for}} タグを利用する方法と、一度に複数のオブジェクトを反復処理する方法を紹介しました。

    

<!-- looping {{for}} -->
{{for students}}
{{/for}}           
<!--  combo iterators {{for}} -->
{{for teachers students staff}}
{{/for}}

{{for}} は (またはどのブロック タグも)、ブロックのコンテンツを外部テンプレートに置き換えることで、(コンテンツがある) 任意のブロック タグから自己終了タグに変換できます。この場合、tmpl プロパティを使って外部テンプレートを宣言によってポイントします。これで、タグはインライン コンテンツの代わりに、外部テンプレートをレンダリングします。

これにより、テンプレートを利用する際に、テンプレート マークアップを別の場所で再利用するモジュール方式を採用しやすくなり、テンプレートの編成や組み立ても簡単になります。

<!--  self closing {{for}} -->
{{for lineItems tmpl="#lineItemsDetailTmpl" /}}

フラットなデータはほとんどないため、オブジェクト階層にアクセスしてデータを取得できることが、テンプレートにとっては重要な機能の 1 つです。前回は、ドット表記と角かっこを使用して、オブジェクト階層内にアクセスする主な手法を紹介しましたが、{{for}} タグを使用すればコードを簡潔にすることも可能です。オブジェクト階層内にアクセスするオブジェクト構造があり、子オブジェクトのプロパティのレンダリングを必要とする場合に、このことがよくわかります。たとえば、person オブジェクトの address をレンダリングする場合、次のようなテンプレートを作成します。このコードでは、パス内に "address" という語が何度も出てきます。

<div>{{:address.street1}}</div>
<div>{{:address.street2}}</div>
<div>{{:address.city}}, {{:address.state}} {{:address.postalCode}}</div>

{{for}} を使うと、次のように、address オブジェクトを繰り返す必要がなくなり、address をレンダリングするコードをかなり簡潔にできます。

<!--  "with" {{for}} -->
{{for address}}
  <div>{{:street1}}</div>
  <div>{{:street2}}</div>
  <div>{{:city}}, {{:state}} {{:postalCode}}</div>
{{/for}}

{{for}} は、address プロパティを処理します。address プロパティは、複数のプロパティが設定された単一オブジェクトで、オブジェクトの配列ではありません。address が true に評価される (false ではない値を保持している) 場合、{{for}} ブロックのコンテンツがレンダリングされます。また、{{for}} によって、現在のデータ コンテキストが person オブジェクトから address オブジェクトに変更されます。つまり、多くのライブラリや言語が実装している "with" コマンドのような働きをします。したがって、前の例では、{{for}} タグによってデータ コンテキストが address に変更された後に、テンプレートのコンテンツが 1 度 (1 つしか address がないため) レンダリングされています。person に address がない (address プロパティが null であるか定義されていない) 場合、コンテンツはまったくレンダリングされません。このため、{{for}} ブロックは、特定の状況でのみ表示するテンプレートの保持に、非常に便利です。次の例 (付属のコード ダウンロードの 08-for-variations.html ファイル) は、{{for}} を使用して、価格情報が存在する場合に価格情報を表示する方法を示しています。

{{for pricing}}
  <div class="text">${{:salePrice}}</div>
  {{if fullPrice !== salePrice}}
    <div class="text highlightText">PRICED TO SELL!</div>
  {{/if}}
{{/for}}

外部テンプレート

コードの再利用は、テンプレートを使用する大きなメリットの 1 つです。テンプレートを使用するページと同じページ内の <script> タグ内にテンプレートが定義されている場合、テンプレートの再利用性は本来よりも限られます。複数のページからアクセスするテンプレートは、専用のファイルに作成し、必要に応じて取得します。JavaScript や jQuery を使うことで、外部ファイルからテンプレートを取得しやすくなり、JsRender を使うことで、外部テンプレートをレンダリングしやすくなります。

外部テンプレートでよく使用する変換の 1 つは、アンダースコアを使用してファイル名にプレフィックスを追加する方法です。これは、部分ビューによく利用される名前付け規則です。また、すべてのテンプレート ファイルの最後に .tmpl.html を付けることもお勧めします。.tmpl は、そのファイルがテンプレートであることを示します。.html は、単純に、Visual Studio などの開発ツールから、テンプレートに HTML が含まれることを認識しやすくします。図 1 は、外部テンプレートのレンダリングを示しています。

図 1 外部テンプレートのレンダリングのコード

my.utils = (function () {
  var
    formatTemplatePath = function (name) {
      return "/templates/_" + name + ".tmpl.html";
    },
    renderTemplate = function (tmplName, targetSelector, data) {
      var file = formatTemplatePath(tmplName);
      $.get(file, null, function (template) {
        var tmpl = $.templates(template);
        var htmlString = tmpl.render(data);
        if (targetSelector) {
          $(targetSelector).html(htmlString);
        }
        return htmlString;
          });
        };
    return {
      formatTemplatePath: formatTemplatePath,
        renderExternalTemplate: renderTemplate
    };
})()

テンプレートを外部ファイルから取得する方法の 1 つは、Web アプリの JavaScript から呼び出せるユーティリティ関数を作成することです。図 1 では、my.utils オブジェクトの renderExternalTemplate 関数が、最初に $.get 関数を使用してテンプレートを取得しています。呼び出しが完了すると、応答のコンテンツから $.templates 関数を使用して、JsRender テンプレートが作成されます。最後に、テンプレートがテンプレート自体の render 関数を使用してレンダリングされ、結果の HTML がターゲットに表示されます。このコードは、テンプレート名、DOM ターゲット、およびデータ コンテキストをカスタムの renderExternalTemplates 関数に渡す次のコードを使用して呼び出すことができます。

my.utils.renderExternalTemplate("medMovie", "#movieContainer", my.vm);

このサンプルの外部テンプレートは、_medMo­vie.tm­pl.html サンプル ファイルにあり、HTML と JsRender タグのみを使用しています。<script> タグによってラップされていません。外部テンプレートにはこの手法を使用することをお勧めします。開発環境がコンテンツを HTML と認識し、特に何もせずに IntelliSense を利用できるので、間違えずにコードを作成しやすくなるためです。ただし、ファイルに複数のテンプレートが含まれ、各テンプレートが <script> タグでラップされ、各テンプレートを一意に識別する ID が指定されていることもあります。これは、外部テンプレートを処理するまた別の方法の 1 つにすぎません。最終結果は 図 2 のようになります。

The Result of Rendering an External Template
図 2 外部テンプレートのレンダリング結果

ビュー パス

JsRender は、現在のビュー オブジェクトへのアクセスを簡単にする、特殊なビュー パスをいくつか提供しています。#view は現在のビューへのアクセスを提供し、#data はビューの現在のデータ コンテキストへのアクセスを提供します。#parent はオブジェクト階層内で上の階層に移動し、#index はインデックス プロパティを返します。

<div>{{:#data.section}}</div>
<div>{{:#parent.parent.data.number}}</div>
<div>{{:#parent.parent.parent.parent.data.name}}</div>
<div>{{:#view.data.section}}</div>

ビュー パス (#view 以外) を使用しているとき、そのビュー パスは現在のビューに対して有効です。つまり、次のコードは機能的に同じです。

#data
#view.data

ビュー パスは、注文の詳細情報がある注文情報を保持した customers などのオブジェクト階層内や、保管場所の倉庫内にあるムービー間を移動するときに便利です (コード ダウンロードのサンプル ファイルの 11-view-paths.html を参照)。

共通式は、ロジックの不可欠な要素の 1 つで、テンプレートのレンダリング方法を決定する際に役に立つ可能性があります。JsRender は、図 3 の式を含む (その他にも) 共通式をサポートしています。

図 3 JsRender の共通式

コメント
+ {{ :a + b }} 加算
- {{ :a - b }} 減算
* {{ :a * b }} 乗算
/ {{ :a / b }} 除算
|| {{ :a || b }} 論理和
&& {{ :a && b }} 論理積
! {{ :!a }} 否定
? : {{ :a === 1 ? b * 2: c * 2 }} 三次式
( ) {{ :(a||-1) + (b||-1) }} かっこを使った評価順序の指定
% {{ :a % b }} 剰余演算
<=、>=、<、> {{ :a <= b }} 比較演算
===、!== {{ :a === b }} 等値と非等値

JsRender は式の評価をサポートしますが、式の割り当てや、ランダム コードの実行はサポートしません。このため、変数の代入を実行する式や、通知ウィンドウを開くなどの操作を実行する式は使用できません。式の目的は、式を評価し、結果をレンダリングするか、結果を基にアクションを起こすか、結果を別の操作で使用することです。

たとえば、JsRender で {{:a++}} を実行すると、変数をインクリメントしようとするため、エラーになります。また、{{:alert('hello')}} も、存在しない #view.data.alert という関数を呼び出そうとするため、エラーになります。

カスタム タグを登録する

JsRender には、カスタム タグ、コンバーター、ヘルパー関数、テンプレート パラメーターなど、いくつか強力な拡張ポイントが用意されています。各拡張ポイントを呼び出す構文は、次のとおりです。

{{myConverter:name}}
{{myTag name}}
{{:~myHelper(name)}}
{{:~myParameter}}

これらはそれぞれ別の目的に使われますが、状況に応じて、やや目的が重なることがあります。これらから使用する構文を選択する方法を説明する前に、それぞれの機能と定義方法を理解しておくことが重要です。

カスタム タグは、"コントロールのような" 機能があり、自己完結型になり得るものをレンダリングする必要があるときに理想的です。たとえば、星による評価は、次のように、単純にデータを使用して数値としてレンダリングできます。

{{:rating}}

ただし、JavaScript ロジックを使用し、CSS と塗りつぶした星と塗りつぶさない星の画像を使って星による評価をレンダリングする方がよい可能性があります。

{{createStars averageRating max=5/}}

星を作成するロジックは、プレゼンテーションから分離できます (分離することをお勧めします)。JsRender は、この機能をラップするカスタム タグを作成する手段を提供しています。図 4 のコードは、createStars という名前のカスタム タグを定義し、これを JsRender に登録して、このスクリプトを読み込むページで使用できるようにしています。このカスタム タグを使用する場合は、このタグの JavaScript ファイルであるサンプル コードの jsrender.tag.js をページに組み込みます。

図 4 カスタム タグの作成

$.views.tags({
  createStars: function (rating) {
    var ratingArray = [], defaultMax = 5;
    var max = this.props.max || defaultMax;
    for (var i = 1; i <= max; i++) {
      ratingArray.push(i <= rating ? 
        "rating fullStar" : "rating emptyStar");
    }
    var htmlString = "";
    if (this.tmpl) {
      // Use the content or the template passed in with the template property.
      htmlString = this. renderContent(ratingArray);
    } else {
        // Use the compiled named template.
        htmlString = $.render.compiledRatingTmpl(ratingArray);
    }
    return htmlString;
  }

カスタム タグには、前のコードにある {{createStars}} の max=5 プロパティのように、宣言型プロパティを指定できます。そのようなプロパティにコードからアクセスするには、this.props を使用します。たとえば、次のコードは、配列を受け取る sort というカスタム タグを登録します (reverse というプロパティが true に設定されて {{sort array reverse=true/}} となっている場合、配列は逆順で返されます)。

$.views.tags({
sort: function(array){
  var ret = "";
  if (this.props.reverse) {
    for (var i = array.length; i; i--) {
      ret += this.tmpl.render(array[i - 1]);
    }
  } else {
      ret += this.tmpl.render(array);
  }
  return ret;
}}

経験則によると、他の要素 (createStars や sort など) をレンダリングする必要があり、タグを再利用できるときにカスタム タグを使用します。タグが 1 回しか必要にならないシナリオには、カスタム タグは理想的ではありません。

コンバーター

カスタム タグがコンテンツの作成に理想的なら、コンバーターは、ソース値を別の値に変換するという単純なタスクに適しています。コンバーターを使用すると、ソース値 (true か false のブール値など) をまったく違うもの (それぞれ、緑色と赤色など) に変更できます。たとえば、次のコードは、priceAlert コンバーターを使用して、salePrice の値に基づいて、価格についての警告を含む文字列を返します。

<div class="text highlightText">{{priceAlert:salePrice}}</div>

コンバーターは、次のように、URL を変更する場合も便利です。

<img src="{{ensureUrl:boxArt.smallUrl}}" class="rightAlign"/>

次のサンプルでは、ensureUrl コンバーターによって boxArt.smallUrl の値が修飾 URL に変換されます (どちらのコンバーターも、12-converters.html ファイルで使用されていて、JsRender $.views.converters 関数を使用して jsrender.helpers.js に登録されています)。

$.views.converters({
  ensureUrl: function (value) {
    return (value ? value : "/images/icon-nocover.png");
  },
  priceAlert: function (value) {0
    return (value < 10 ? "1 Day Special!" : "Sale Price");
  }
});

コンバーターは、データをレンダリング値に変換する際にパラメーターを使用しません。パラメーターが必要なシナリオでは、コンバーターよりもヘルパー関数かカスタム タグの方が適しています。以前に説明したように、カスタム タグには名前付きパラメーターを使用できるので、createStars タグにパラメーターを指定して、星のサイズや色、星に適用する CSS クラスなどを定義できます。ここで重要なポイントは、コンバーターは単純な変換に使い、カスタム タグは複数の要素を一括で処理するレンダリングに使用するということです。

ヘルパー関数とテンプレート パラメーター

2 とおりの方法で、テンプレートのレンダリング中に、ヘルパー関数またはパラメーターを渡して使用することができます。1 つは $.views.helpers を使って登録する方法で、タグやコンバーターを登録する方法と似ています。

$.views.helpers({
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

これで、アプリケーションのすべてのテンプレートで、ヘルパー関数またはパラメーターが利用可能になります。もう 1 つは、レンダリングの呼び出しにオプションとして渡す方法です。

$.render.myTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

このコードは、その特定のテンプレート レンダリング呼び出しのコンテキストでのみ、ヘルパー関数またはパラメーターを利用可能にします。どちらの方法でも、"~" をプレフィックスとしてパラメーターまたは関数名に付けることで、テンプレート内からヘルパーにアクセスできます。

{{: ~extPrice(~todaysPrices.unitPrice, qty) }}

ヘルパー関数は、データの変換、計算の実行、アプリケーション ロジックの実行、配列やオブジェクトを返す、さらにはテンプレートを返すなど、ほぼあらゆる操作を実行できます。

たとえば、製品の配列内を検索し、ギター製品をすべて見つける、getGuitars というヘルパー関数を作成できます。また、このヘルパー関数で、ギターの種類を表すパラメーターを受け取ることもできます。結果は、単一値のレンダリングに使用することも、結果の配列の反復処理に使用することもできます (ヘルパー関数はどのような結果も返すことができるため)。次のコードは、アコースティック ギター製品をすべて含む配列を取得し、{{for}} ブロックを使用して、それらを反復処理します。

{{for ~getGuitars('acoustic')}} ... {{/for}}

ヘルパー関数から他のヘルパー関数を呼び出すこともでき、注文の品目の配列を使用して合計価格を計算し、割引率や税率を適用するなどの処理が可能です。

{{:~totalPrice(~extendedPrice(lineItems, discount), taxRate}}

複数のテンプレートからアクセスできるヘルパー関数は、ヘルパー関数を含むオブジェクト リテラルを JsRender $.views.helpers 関数に渡すことで定義します。次の例では、concat 関数を定義して、複数の引数を連結しています。

$.views.helpers({
  concat:function concat() {
    return "".concat.apply( "", arguments );
  }
})

concat ヘルパー関数は、{{:~concat(first, age, last)}} を使用して呼び出すことができます。最初、中央、最後の値にアクセスでき、それぞれ John、25、Doe だとすると、John25Doe がレンダリングされます。

ユニークなシナリオでのヘルパー関数

特定のテンプレートにヘルパー関数を使用したいが、他のテンプレートでは再利用したくない状況が発生する可能性があります。たとえば、ショッピング カート テンプレートには、そのテンプレート固有の計算が必要になることが考えられます。ヘルパー関数によってその計算を実行できますが、他のすべてのテンプレートからそのヘルパー関数にアクセスできるようにする必要はありません。JsRender は、前述の 2 番目の方法、つまり、レンダリング呼び出しのオプションを使って関数を渡すことで、このシナリオをサポートします。

$.render.shoppingCartTemplate( data, {
  todaysPrices: { unitPrice: 23.40 },
  extPrice:function(unitPrice, qty){
    return unitPrice * qty;
  }
});

この場合、ショッピング カート テンプレートはレンダリングされ、このテンプレートの計算に必要なヘルパー関数とテンプレート パラメーターが、レンダリング呼び出しによって直接提供されます。ここで重要なのは、この場合、ヘルパー関数がこの特定のテンプレートのレンダリング中にのみ存在することです。

どれを使用するか

JsRender は、コンバーター、カスタム タグ、およびヘルパー関数を使用して強力なテンプレートを作成するオプションをいくつか提供していますが、それぞれどのシナリオで使用すべきかを知っておくことが重要です。経験則からすると、図 5 のようなデシジョン ツリーを使用するのが有効です。このツリーは、これらの機能のどれを使用するかを決定する方法を表しています。

図 5 適切なヘルパーを選ぶためのデシジョン ツリー

if (youPlanToReuse) {
  if (simpleConversion && !parameters){
    // Register a converter.
  }
  else if (itFeelsLikeAControl && canBeSelfContained){
    // Register a custom tag.
  }
  else{
    // Register a helper function.
  }
}
else {
  // Pass in a helper function with options for a template.
}

関数が一度しか使用されない場合は、アプリケーション全体で使用できるようにするオーバーヘッドを生み出す必要はありません。これは、必要なときに渡される "1 度だけの" ヘルパー 関数に理想的な状況です。

コードを許可する

テンプレート内にカスタム コードを作成した方が、簡単な場合があります。JsRender ではコードを埋め込むことができますが、これは他の方法がどれも使用できなかった場合にのみ採用することをお勧めします。コードは、プレゼンテーションと動作が混在しているので、メンテナンスが難しいためです。

コードは、アスタリスクをプレフィックスにしたブロック {{* }} でラップし、allowCode を true に設定することで、テンプレート内に埋め込むことができます。たとえば、myTmpl という名前のテンプレート (図 6 参照) には、コマンドや、一連の言語の中で "and" という語をレンダリングする適切な場所を評価するコードが埋め込まれています。完全なサンプルについては、13-allowcode.html ファイルを参照してください。ロジックはそれほど複雑ではありませんが、テンプレート内ではコードは読みにくい可能性があります。

JsRender では、allowCode プロパティが true に設定されない限り (既定値は false)、コードの実行を許可しません。次のコードは、movieTmpl という複雑なテンプレートを定義し、これを図 6 のスクリプト タグのマークアップに割り当てて、テンプレートの allowCode を有効にしています。

$.templates("movieTmpl", {
  markup: "#myTmpl",
  allowCode: true
});
$("#movieRows").html(
  $.render.movieTmpl(my.vm.movies)
);

テンプレートが作成されたら、レンダリングされます。allowCode 機能は、読み難いコードにつながる可能性があり、場合によっては、ヘルパー関数で目的の処理に対応できることもあります。たとえば、図 6 の例では、JsRender の allowCode を使用して、必要な場所にコンマと "and" という単語を追加しています。ただし、これも、ヘルパー関数を作成することで対応できます。

$.views.helpers({
  languagesSeparator: function () {
    var view = this;
    var text = "";
    if (view.index === view.parent.data.length - 2) {
      text = " and";
    } else if (view.index < view.parent.data.length - 2) {
      text = ",";
    }
    return text;
  }
})

図 6 テンプレートでのコードの許可

<script id="myTmpl" type="text/x-jsrender">
  <tr>
    <td>{{:name}}</td>
    <td>
      {{for languages}}
        {{:#data}}{{*
          if ( view.index === view.parent.data.length - 2 ) {
        }} and {{*
          } else if ( view.index < view.parent.data.length - 2 ) {
        }}, {{* } }}
      {{/for}}
    </td>
  </tr>
</script>

この languagesSeparator ヘルパー関数は、"~" というプレフィックスを名前に付けることで呼び出されます。これにより、次のように、ヘルパーを呼び出すテンプレート コードが、はるかに読みやすくなります。

{{for languages}}
  {{:#data}}{{:~languagesSeparator()}}
{{/for}}

ロジックをヘルパー関数に移動することで、テンプレートから動作が取り除かれ、JavaScript に移動されました。これは、好ましい分離パターンに従っています。

パフォーマンスと柔軟性

JsRender は、プロパティ値のレンダリングだけでなく、複雑な式のサポート、{{for}} タグを使用したコンテキストの反復処理と変更、コンテキストを移動するためのビュー パスなど、さまざまな機能を提供しています。また、カスタム タグ、コンバーター、ヘルパーを必要に応じて追加することで、機能を拡張する手段も提供しています。これらの機能と、純粋に文字列ベースでのテンプレートの処理アプローチによって、JsRender はすばらしいパフォーマンスのメリットと、非常に高い柔軟性を実現しています。

John Papa は、以前は Silverlight と Windows 8 チームのマイクロソフトのエバンジェリストで、人気のあった「Silverlight TV」ショーの司会をしていました。これまで、BUILD、MIX、PDC、TechEd、Visual Studio Live!、DevConnections など、世界各地で開催されるイベントの基調講演やセッションでスピーカーを務めています。Papa は、Microsoft Regional Director、Visual Studio Magazine のコラムニスト (「Papa’s Perspective」)、Pluralsight によるトレーニング ビデオの作者の 1 人でもあります。Twitter (twitter.com/john_papa) で彼をフォローしてください。

この記事のレビューに協力してくれた技術スタッフの Boris Moore に心より感謝いたします。