Vue.jsのv-htmlで書き出したHTMLを非同期的にjQueryで更新する

![goal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/0f5ab415-8732-f9bc-42fd-09c01f0d354d.gif)

Vue.jsのv-htmlで書き出したHTMLを非同期で書き換えてみましょう。
サンプルではユーザーの入力した文字をv-htmlで表示しています。

書き出したHTMLにimgが含まれていた場合、imgのdata-nameを元に非同期で画像データを引っ張ってきて表示する、という想定です。
（画像データを用意すると一手間かかるため、今回は代わりに絵文字を表示しています。）

**自己紹介**
アプリクリエイターの **ミャウチー** です。HTMLとCSS歴は8年になります。
アプリを作っている最中に今回の問題にぶつかって自己解決したので、ノウハウを共有します。

**今回の前提条件**
HTML, CSS, JavaScriptを使います。
初歩レベルのjQueryを使います。
Vue.jsは `data: { 〜 }` や `@click` が分かっている前提です。
Vue.jsはCDNで実行しています。

**もくじ**
1. 初期状態の確認
2. Vueデータに変更があったときアラートを出す
3. v-htmlで書き出されたときアラートを出す
4. 書き出されたHTMLを取得して非同期処理をする
5. 待機中に [loading...] を出す

**注意**
今回作成するものは、非同期である必要のない処理を非同期に実行しています。
今回の目的が「Vue.jsのv-htmlで書き出したHTMLを非同期的にjQueryで更新する」であり、問題を単純化するためです。
実際の場面では今回の例を応用して、何かしらのAPIを非同期で実行することを想定しています。

<details>
<summary>**先に完成形のコードをみたい方は、この行をクリックすると下に展開されます。**</summary>
<div>

**HTML:**

```HTML
<main>
	<input type='text' v-model='input'>
	<input type='button' value='↓' @click="output = input"> HTML
	<p v-html='output'></p>
</main>
```

**CSS:**

```CSS
img[src='']::before {
	content: "[ loading... ]";
}
```

**JavaScript:**

```JavaScript
const vm = new Vue({
	el: 'main',

	data: {
		input: '',
		output: '',
	},
	
	watch: {
		output: function(val) {
			const $dom = $(this.$el)
			
			this.$nextTick(function() {
				// 非同期的な何かしらの処理
				setTimeout(function() {

					// data-nameで指定した値を取得
					const name = $dom.find('img').data('name')

					// 今回は簡易例として画像を絵文字に置き換えます
					const character = (name === 'cat') ? '🐱' : '👻'
					$dom.find('img').replaceWith(`<span>${character}</span>`)

					// APIで取得した画像などを表示する場合はこちら
					//$dom.find('img').attr('src', "https://〜画像のURL〜.jpg")

				}, 2000)    // = 例として2秒後に実行
			})
		},
	},
})
```

</div>
</details>

# STEP 1 初期状態の確認
![01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/3698cc07-057c-f003-d073-3fe7f7a11977.gif)

**HTML:**
・ユーザーの入力ボックス
・反映ボタン
・HTMLを書き出す領域
HTMLはこれ以上変更しません。

```HTML
<main>
	<input type='text' v-model='input'>
	<input type='button' value='↓' @click="output = input"> HTML
	<p v-html='output'></p>
</main>
```

**CSS:**
空っぽの状態から始めます。

```CSS
/* なし */
```

**JavaScript:**

```JavaScript
const vm = new Vue({
	el: 'main',
	
	data: {
		input: '',
		output: '',
	},
})
```

**実行の流れ:**
1. **変数inputにユーザーの入力した文字が入る**
   （例: `hello <img data-name='cat' src=''> world!`）
2. **「↓」ボタンを押すと 変数output = input となる**
3. **変数outputがHTMLとして書き出される**
  （書き出されたとき`<img〜>`が表示されないのは、src属性が空っぽなためです。）

# STEP 2 Vueデータに変更があったときアラートを出す
![02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/0602d9de-c03c-70f9-9438-2be22817f965.gif)

`new Vue({ 〜 })` 内の一番下に次のコードを足します。

```JavaScript
watch: {
	output: function(val) {
		alert("outputの中身が変わりました")
	},
},
```

`output` の変更をwatchすることで、変更があったときに関数が実行されます。

# STEP 3 v-htmlで書き出されたときアラートを出す
![03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/e0f84a97-315e-beb0-b0eb-b03e581a2a24.gif)

`output: function(val) { 〜 }` を次のように書き換えます。

```JavaScript
output: function(val) {
	this.$nextTick(function() {
		alert("HTMLが書き出されました")
	})
},
```

`$nextTick()` は、DOMが書き換えられた後に呼び出されます。
参照：[Vue.js リアクティブの探求 #非同期更新キュー](https://jp.vuejs.org/v2/guide/reactivity.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E6%9B%B4%E6%96%B0%E3%82%AD%E3%83%A5%E3%83%BC)

体感的にはalertの出るタイミングがSTEP 2の時と変わりないですが、若干違います。
STEP 2のとき：DOMが書き換えられる前
STEP 3のとき：DOMが書き換えられた後
(alertと同じタイミングで `console.log(this.$el.innerHTML)` などとDOMの中身をコンソール出力してみると分かりやすいです)

# STEP 4 書き出されたHTMLを取得して非同期処理をする
![04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/d4cb0ef6-390d-4195-9f1b-e1b192c81d9b.gif)

**STEP 4 - 1**
`output: function(val) { 〜 }` 内の一番上に `const $dom = $(this.$el)` を書き足します。

変数$domには、v-htmlで書き出されたHTMLを含むjQueryオブジェクトが代入されます。
（厳密には、今回の場合 `$dom[0] === $('main')[0]` です。）

**STEP 4 - 2**
`this.$nextTick(function() { 〜 })` の中に非同期的な何かしらの処理を書きます。
今回は例として、DOMが変更された2秒後にimgを絵文字に置き換えます。

```JavaScript
this.$nextTick(function() {

	/* * * 非同期的な何かしらの処理 * * */
	setTimeout(function() {

		// data-nameで指定した値を取得
		const name = $dom.find('img').data('name')

		// 今回は簡易例として画像を絵文字に置き換えます
		const character = (name === 'cat') ? '🐱' : '👻'
		$dom.find('img').replaceWith(`<span>${character}</span>`)

		// APIで取得した画像などを表示する場合はこちら
		//$dom.find('img').attr('src', "https://〜画像のURL〜.jpg")

	}, 2000)    // = 例として2秒後に実行
})
```


# STEP 5 待機中に [loading...] を出す
![05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/3236f07c-a167-a5e0-8a34-4297e6ce355c.gif)

DOMが書き出されてから絵文字が表示されるまでの間、代わりの文字を出しておきます。

今回は **imgタグのsrc属性が空っぽ** のとき、画像の代わりに `[ loading... ]` を表示します。
例： `<img src=''>`

**CSS:**

```CSS
img[src='']::before {
	content: "[ loading... ]";
}
```

# まとめ
![goal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/ef423dbd-9420-88d6-d639-8e5c810c1d3e.gif)

今回はVue.jsのv-htmlで書き出したHTMLを非同期で書き換えました。
応用としては、画像データを何かのAPIで非同期に取得したいときに使えるかと思います。

私の場合、Firebase Firestoreから文書データを引っ張ってきたときに、テキストデータをまず先に表示して、画像が含まれていたらFirebase Storageから非同期に取得する、としようとしました。
それで今回のことが分からずつまずき、「[Vue.js リアクティブの探求 #非同期更新キュー](https://jp.vuejs.org/v2/guide/reactivity.html#%E9%9D%9E%E5%90%8C%E6%9C%9F%E6%9B%B4%E6%96%B0%E3%82%AD%E3%83%A5%E3%83%BC)」を解読しながら自己解決したので、共有しました。

実際にv-htmlを使う場合は「[Vue.js テンプレート構文 #生の HTML](https://jp.vuejs.org/v2/guide/syntax.html#%E7%94%9F%E3%81%AE-HTML)」に書かれている通り、**XSSに気をつけてください。**

**HTML:**

```HTML
<main>
	<input type='text' v-model='input'>
	<input type='button' value='↓' @click="output = input"> HTML
	<p v-html='output'></p>
</main>
```

**CSS:**

```CSS
img[src='']::before {
	content: "[ loading... ]";
}
```

**JavaScript:**

```JavaScript
const vm = new Vue({
	el: 'main',

	data: {
		input: '',
		output: '',
	},
	
	watch: {
		output: function(val) {
			const $dom = $(this.$el)
			
			this.$nextTick(function() {
				// 非同期的な何かしらの処理
				setTimeout(function() {

					// data-nameで指定した値を取得
					const name = $dom.find('img').data('name')

					// 今回は簡易例として画像を絵文字に置き換えます
					const character = (name === 'cat') ? '🐱' : '👻'
					$dom.find('img').replaceWith(`<span>${character}</span>`)

					// APIで取得した画像などを表示する場合はこちら
					//$dom.find('img').attr('src', "https://〜画像のURL〜.jpg")

				}, 2000)    // = 例として2秒後に実行
			})
		},
	},
})
```

質問や気になる点があれば、コメントやTwitterで連絡ください！

# ミャウチー
高校一年の頃からアプリ作りに没頭していて、今はアプリクリエイターとしての人生を主軸に生きています。
おじいさんになっても縁側に座ってカタカタとアプリ作りをします。最近はアウトプットを大切にしようと奮闘中です。
サイバーブレイン株式会社( メインサービスは[AI Academy](https://aiacademy.jp) )でデザイナーをしています。

**Twitter | Miyauchi Akira**
https://twitter.com/miyauchoi

<img width="1674" alt="portfolio.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/3748d9b3-a744-3d59-9ed6-a3c188531580.png">
**ポートフォリオ | ミヤウチアキラ**
https://miyauchi-akira.app

![note.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268454/41f457ec-5316-35af-3503-a00bf607bdd8.png)
**アプリデザインで考えていること**
[「ポエティカルデザイン」というアプリデザイン手法 | note](https://note.mu/miyauchoi/n/nbd8cd775ecbb)