職人的なDOM生成コードをテンプレートエンジンに置き換える

# 背景

稼働中のアプリケーションを変更するにあたって、職人的なDOM生成コードを読み取ることが困難なことがあります。
例えば、次のように職人的にHTML Document Objectを作り

```js
function createElement(id, data) {
  const element = document.createElement('div')
  element.setAttribute('id', id)
  element.setAttribute('title', 'id: ' + data.id + ', pred: ' + data.pred + ', value: ' + data.value)
  element.setAttribute('origin-id', data.id)
  element.setAttribute('type', data.value)
  element.setAttribute('pred', data.pred)
  element.classList.add('textae-editor__attribute')
  element.innerText = data.value

  const popUpEditorElement = document.createElement('div')
  popUpEditorElement.classList.add('textae-editor__attribute-buttons')

  const editButtonElement = document.createElement('div')
  editButtonElement.classList.add('textae-editor__attribute-button')
  editButtonElement.classList.add('textae-editor__attribute-button--edit')
  editButtonElement.setAttribute('title', 'Edit this attribute.')

  const deleteButtonElement = document.createElement('div')
  deleteButtonElement.classList.add('textae-editor__attribute-button')
  deleteButtonElement.classList.add('textae-editor__attribute-button--delete')
  deleteButtonElement.setAttribute('title', 'Delete this attribute.')

  popUpEditorElement.appendChild(editButtonElement)
  popUpEditorElement.appendChild(deleteButtonElement)

  element.appendChild(popUpEditorElement)
}
```

jQuery.append()を使ってDOMを追加するソースコードがあるとします。

```js
$(parent).append(createElement(id, data))
```

# 課題

このソースコードからレンダリングされるHTMLを直感的に読み取るのは難しいです。
DOMの構造を修正したいときに変更箇所の特定が困難です。
よくやる対応では、ソースコードを少し変えてレンダリングされる内容の変化を確認しながら、変更箇所を特定していきます。

対応できますが、変更頻度が高い時は手間が馬鹿になりません。

# リファクタリング手法

テンプレートエンジンを導入し、ソースコード上にレンダリング後のHTMLのイメージをそのまま記述します。
ここでは[Handlebars.js](https://handlebarsjs.com/)を使います。

## 手順１. Google Chromeの開発コンソールを使ってレンダリング後のHTMLを取得します。

Google Chromeで対象となるDOMを選択肢、コンテキストメニューから Copy > Copy outerHTML を選択します。

![スクリーンショット 2019-06-17 12.26.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/15964/d01b3ace-5dd1-da51-a70b-5b39dee1210b.png)

次のHTMLが取得できます。

```html
<div id="editor1__AA1" title="id: A1, pred: example_predicate_1, value: attr1" origin-id="A1" type="attr1" pred="example_predicate_1" class="textae-editor__attribute">attr1
  <div class="textae-editor__attribute-buttons">
    <div class="textae-editor__attribute-button textae-editor__attribute-button--edit" title="Edit this attribute."></div>
    <div class="textae-editor__attribute-button textae-editor__attribute-button--delete" title="Delete this attribute."></div>
  </div>
</div>
```

## 手順２. テンプレートで置き換える場所を設定する

Handlebars.jsでは`{{`と`}}`で括られた文字列を指定の値で置き換えます。
JavaScriptでは[テンプレート文字列](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/template_strings)をつかうと、改行を含んだ文字列をソースコード中に記述できます。
次のようなソースコードになります。

```js
const source = `
<div id="{{id}}" title="{{title}}" origin-id="{{originId}}" type="{{value}}" pred="{{pred}}" class="textae-editor__attribute">{{value}}
  <div class="textae-editor__attribute-buttons">
    <div class="textae-editor__attribute-button textae-editor__attribute-button--edit" title="Edit this attribute."></div>
    <div class="textae-editor__attribute-button textae-editor__attribute-button--delete" title="Delete this attribute."></div>
  </div>
</div>
`
```

## 手順３. HTMLを生成する

これらを組み合わせてHTMLを生成するコードを作成します。
例えば次のようなソースコードになります。

```js
import Handlebars from 'handlebars'

const source = `
<div id="{{id}}" title="{{title}}" origin-id="{{originId}}" type="{{value}}" pred="{{pred}}" class="textae-editor__attribute">{{value}}
  <div class="textae-editor__attribute-buttons">
    <div class="textae-editor__attribute-button textae-editor__attribute-button--edit" title="Edit this attribute."></div>
    <div class="textae-editor__attribute-button textae-editor__attribute-button--delete" title="Delete this attribute."></div>
  </div>
</div>
`
const template = Handlebars.compile(source)

function createHtml(id, attribute) {
  return template({
    id,
    title: `id: ${attribute.id}, pred: ${attribute.pred}, value: ${attribute.value}`,
    originId: attribute.id,
    value: attribute.value,
    pred: attribute.pred
  })
}
```

この関数は`createElement`関数と、同等の内容を生成します。
違いは生成するものがDOM ElementかHTML文字列かの違いです。

## 手順4. createElementをcreateHtmlに置き換える

[jQuery.append](http://api.jquery.com/append/)は引数にHTML文字列を受け取れます。
呼び出す関数を置き換えるだけで対応が完了します。

```js
$(parent).append(createHtml(id, data))
```



## おまけ
jQueryを使わずに[Node.appendChild()](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)を使っている場合、例えば、次のソースコードの場合

```js
parent.appendChild(createElement(id, data))
```

Node.appendChild()は引数にDOM要素しか受け付けません。
DOMの任意の場所にHTML文字列を挿入するには[element.insertAdjacentHTML](https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)を使います。


```js
parent.insertAdjacentHTML('beforeend', createHtml(id, data))
```

## 参考

- [innerHTML より insertAdjacentHTML を使う - Qiita](https://qiita.com/amamamaou/items/624c22adec32515e863b)