4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaScript その2Advent Calendar 2020

Day 24

Handlbars.jsをテンプレートリテラルで置き換える手順

Last updated at Posted at 2020-12-23

背景

HTMLを生成するためのテンプレートエンジンとしてHandlebars.jsがあります。
HandlebarsはMustacheの記法{{}}を引き継いだJavaScriptのテンプレートエンジンです。
テンプレートを事前コンパイルすることで、Mustacheより高速に動作するため人気がありました。

個人的には{{}}記法が、HTMLと混ざった時に見分けやすく好きです。
しかし、Webpack等のモジュールバンドラーを使った開発では辛い点が出てきました。

Handlebarsは部分テンプレートを定義できます。
定義した部分テンプレートは次のように登録します。

Handlebars.registerPartial('myPartial', '{{prefix}}');

一見問題ないように見えます。
しかし部分テンプレートはグローバルに登録されます。
このため、部分テンプレートの登録は、モジュールの依存関係とは異なる順序で行う必要があります。
例えば次の2つです。

  1. アプリケーション全体の初期化時
  2. (重複に目をつむるって)部分テンプレートを使用するすべてのモジュールで登録

しかし、どちらの手法をとっても、モジュールバンドラーを使った開発とは、あまりしっくり来るものではありません。
モジュールバンドラーを使うときは、

  1. 部分テンプレートを使わなくなったときはバンドルしないで欲しい
  2. 使うときは1回だけバンドルして欲しい

このために、部分テンプレートをモジュールにして、importしたモジュールで使えるようにしたいです。

目的

ES6から導入されたテンプレートリテラルがあります。
これを使ってHandlebarsのテンプレートを置き換えられます。

例えば、Handlebars.jsを使った次のテンプレートがあります。

import Handlebars from 'handlebars'

const template = Handlebars.compile(`
<div class="edit-value-and-pred-dialog__container">
  <div class="edit-value-and-pred-dialog__input-box">
    <label>Predicate:</label><br>
    <input 
      class="value-and-pred-dialog--predicate" 
      value="type"
      disabled="disabled">
  </div>
  <div class="edit-value-and-pred-dialog__input-box ui-front">
    <label class="edit-value-and-pred-dialog--label">Value:<span>{{label}}</span></label><br>
    <input
     class="edit-value-and-pred-dialog--value promise-daialog__observable-element"
     value="{{value}}">
  </div>
</div>`)

テンプレートリテラルで置き換えると次のようになります。

function template(context) {
  const { label, value } = context
  return `
<div class="edit-value-and-pred-dialog__container">
  <div class="edit-value-and-pred-dialog__input-box">
    <label>Predicate:</label><br>
    <input 
      class="edit-value-and-pred-dialog--predicate" 
      value="type" 
      disabled="disabled">
  </div>
  <div class="edit-value-and-pred-dialog__input-box ui-front">
    <label class="edit-value-and-pred-dialog--label">Value:<span>${label}</span></label><br>
    <input 
      class="edit-value-and-pred-dialog--value promise-daialog__observable-element" 
      value="${value}">
  </div>
</div>`
}

テンプレートリテラルはES6の一部なので、当然モジュールにすることも可能です。
また、テンプレートリテラルから、他のテンプレートリテラルを呼び出すことも可能です。
これによって、モジュールバンドラーフレンドリーな開発が可能です。

置き換え手順

Handlebarsテンプレートはテンプレートリテラルで論理的に置き換え可能です。
すでに実装済みのHandlebarsテンプレートがある場合は、どのような手順で置き換えていけばいいでしょうか?
なるべく、安全な手順がうれしいです。

次の手順を考えました。

関数宣言する

Handlebars.compilecontextオブジェクトを引数として、文字列を返却する関数を生成します。
関数のシグネチャーは変更しません。
次のような関数を定義します。

function template(context) {
  return ``
}

関数名はHandlebars.compileの戻り値を代入している変数名と同一とします。
これによりHandlebarsテンプレートの呼び出し側には修正が不要になります。

テンプレートの貼り付け

作成した関数の空テンプレートリテラル部分にHandlebars用のテンプレートをそのまま貼り付けます。

function template(context) {
  return `
<div class="edit-value-and-pred-dialog__container">
  <div class="edit-value-and-pred-dialog__input-box">
    <label>Predicate:</label><br>
    <input 
      class="value-and-pred-dialog--predicate" 
      value="type"
      disabled="disabled">
  </div>
  <div class="edit-value-and-pred-dialog__input-box ui-front">
    <label class="edit-value-and-pred-dialog--label">Value:<span>{{label}}</span></label><br>
    <input
     class="edit-value-and-pred-dialog--value promise-daialog__observable-element"
     value="{{value}}">
  </div>
</div>`
}

この時点で動作はしますが、返却する文字列のプレースホルダーには変数は適用されず、{{label}}のようにそのまま出力されます。

プレースホルダーの置き換え

{{${}}}に置き換えます。
エディターの置換機能を使うとよいでしょう。

function template(context) {
  return `
<div class="edit-value-and-pred-dialog__container">
  <div class="edit-value-and-pred-dialog__input-box">
    <label>Predicate:</label><br>
    <input 
      class="value-and-pred-dialog--predicate" 
      value="type"
      disabled="disabled">
  </div>
  <div class="edit-value-and-pred-dialog__input-box ui-front">
    <label class="edit-value-and-pred-dialog--label">Value:<span>${label}</span></label><br>
    <input
     class="edit-value-and-pred-dialog--value promise-daialog__observable-element"
     value="${value}">
  </div>
</div>`
}

この時点で、エディターにVSCodeなどを使っていれば、labelvalueが宣言されていないことがわかります。

Handlebarsのテンプレートで、if, unless, eachなどを使っている場合、JavaScriptのシンタックスエラーになります。つまり「labelvalueが宣言されていないこと」がわかりません。
その場合は、後述のifの置き換え、eachの置き換えを先に実行してください。

変数宣言

labelvaluecontext引数のプロパティに存在するはずです。
これを変数として宣言します。
分割代入を使うと楽に宣言できます。

const { label, value } = context

function template(context) {
  const { label, value } = context
  return `
<div class="edit-value-and-pred-dialog__container">
  <div class="edit-value-and-pred-dialog__input-box">
    <label>Predicate:</label><br>
    <input 
      class="value-and-pred-dialog--predicate" 
      value="type"
      disabled="disabled">
  </div>
  <div class="edit-value-and-pred-dialog__input-box ui-front">
    <label class="edit-value-and-pred-dialog--label">Value:<span>${label}</span></label><br>
    <input
     class="edit-value-and-pred-dialog--value promise-daialog__observable-element"
     value="${value}">
  </div>
</div>`
}

変数の値がundefinedまたはnullになるとき

Handlebarsでは{{value}}valueundefinedまたはnullのとき空文字に置き換わります。
テンプレートリテラルでは${value}valueundefinedまたはnullのとき"undefined"または"null"に置き換わります。

このときの動作をあわせるために初期値として空文字を指定する必要があります。
${value || ''}と書きましょう。

変数valueundefinedまたはnullになるかわからないとき、
例えば、

  • 仕様が不明
  • 十分なテストで「変数valueundefinedまたはnullにならないこと」が担保できない

場合は常に${value || ''}に置き換えても良いと思います。

応用編

HandlebarsにはBuilt-in Helperがあります。
これを使ってテンプレート中にロジックを書くことができます。
代表的なものにif/elseとeachがあります。

if/elseの置き換え

if/elseは条件演算子で置き換えます。
例えば次のHandlebarsテンプレートは

{{#if showDefault}}
  <div class="create-attribute-definition-dialog__default">
    <label>Default:</label><br>
    <input value="{{defaultValue}}">
  </div>
{{/if}}

テンプレートリテラルでは次のようになります。

${
  showDefault
    ? `
  <div class="create-attribute-definition-dialog__default">
    <label>Default:</label><br>
    <input value="${defaultValue}">
  </div>
`
    : ``
    }

まず、{{#if showDefault}}${showDefault ? `` : ``}に置き換えます。
次に、条件演算子の第二項にifの中身をコピペします。

${showDefault ? `
  <div class="create-attribute-definition-dialog__default">
    <label>Default:</label><br>
    <input value="{{defaultValue}}">
  </div>
` : ``}

プレースホルダーを置き換えます。

${showDefault ? `
  <div class="create-attribute-definition-dialog__default">
    <label>Default:</label><br>
    <input value="${defaultValue}">
  </div>
` : ``}

条件演算子が読みにくい場合は、関数として抽出すると良いでしょう。

${inputDefault(showDefault, defaultValue)}

// 中略

function inputDefault(showDefault, defaultValue) {
  if(showDefault) {
    return `
      <div class="create-attribute-definition-dialog__default">
        <label>Default:</label><br>
        <input value="${defaultValue}">
      </div>`
  } 

  return ''
}

関数の抽出はVSCodeのJS Refactor Extensionを使うと、安全に実行できます。

eachの置き換え

Handlebarsのeachは配列を受け取り、繰り返し文字列に変換します。
例えば次のテンプレートがあります。

{{#each types}}
<tr class="type-pallet__row" style="background-color: {{color}};">
  <td class="type-pallet__label" data-id="{{id}}">
    <span title={{id}}>
      {{id}}
    </span>
  </td>
  <td class="type-pallet__short-label">
    {{label}}
  </td>
  <td class="type-pallet__table-buttons">
    <button
      type="button"
      class="type-pallet__table-button type-pallet__select-all
      title="Select all the cases of this type."
      data-id="{{id}}"
      data-use-number="{{useNumber}}">
    </button>
  </td>
</tr>
{{/each}}

テンプレートリテラルで置き換えると、次のようになります。

${types
  .map(
    ({ color, id, label, useNumber }) => `
<tr class="type-pallet__row" style="background-color: ${color};">
  <td class="type-pallet__label" data-id="${id}">
    <span title=${id}>
      ${id}
    </span>
  </td>
  <td class="type-pallet__short-label">
    ${label || ''}
  </td>
  <td class="type-pallet__table-buttons">
    <button
      type="button"
      class="type-pallet__table-button type-pallet__select-all"
      title="Select all the cases of this type."
      data-id="${id}"
      data-use-number="${useNumber}">
    </button>
  </td>
</tr>
`
  )
  .join('\n')}

配列を繰り返して文字列に変換するのでmapを使います。
{{#each types}}${types.map(() => ``)}に置き換えます。

テンプレートリテラル内で配列を返すと,で結合されます。
事前に好みの文字で結合します。
ここではHTMLを変換するので改行に置き換えます。
${types.map(() => ``).join('\n')}
ブラウザ上でレンダリングされなければ空文字でも空白文字でも構いません。

テンプレートリテラル部にHandlebarsテンプレートをコピペします。

${types.map(() => `
<tr class="type-pallet__row" style="background-color: {{color}};">
  <td class="type-pallet__label" data-id="{{id}}">
    <span title={{id}}>
      {{id}}
    </span>
  </td>
  <td class="type-pallet__short-label">
    {{label}}
  </td>
  <td class="type-pallet__table-buttons">
    <button
      type="button"
      class="type-pallet__table-button type-pallet__select-all
      title="Select all the cases of this type."
      data-id="{{id}}"
      data-use-number="{{useNumber}}">
    </button>
  </td>
</tr>
`).join('\n')}

プレースホルダーを置き換えます。

${types.map(() => `
<tr class="type-pallet__row" style="background-color: ${color};">
  <td class="type-pallet__label" data-id="${id}">
    <span title=${id}>
      ${id}
    </span>
  </td>
  <td class="type-pallet__short-label">
    ${label}
  </td>
  <td class="type-pallet__table-buttons">
    <button
      type="button"
      class="type-pallet__table-button type-pallet__select-all
      title="Select all the cases of this type."
      data-id="${id}"
      data-use-number="${useNumber}">
    </button>
  </td>
</tr>
`).join('\n')}

変数宣言します。
Handlebarsのeachでは、繰り返し対象の配列の要素のプロパティを参照します。
mapにわたす関数の第一引数から取得できます。
ここでも分割代入を使うと便利です。

${types.map(({ color, id, label, useNumber }) => `
<tr class="type-pallet__row" style="background-color: ${color};">
  <td class="type-pallet__label" data-id="${id}">
    <span title=${id}>
      ${id}
    </span>
  </td>
  <td class="type-pallet__short-label">
    ${label}
  </td>
  <td class="type-pallet__table-buttons">
    <button
      type="button"
      class="type-pallet__table-button type-pallet__select-all
      title="Select all the cases of this type."
      data-id="${id}"
      data-use-number="${useNumber}">
    </button>
  </td>
</tr>
`).join('\n')}

また関数を抽出すると見た目はスッキリします。

${types.map(toRow).join('\n')}

関数の抽出はVSCodeのJS Refactor Extensionを使うと、安全に実行できます。

参考

4
2
3

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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?