templateタグ部分をJSの文字列で動的に生成したかった
私自身がこれを実現する必要に迫られて習得した内容なので復習と備忘録の意味合いでも記録を残したいと思います。
Vue.jsの *.vueファイルの構成は
// テンプレート部
<template>
...
</template>
// コード部
<script>
...
</script>
// スタイル部
<style>
...
</style>
のようにテンプレート部、コード部、スタイル部の大きく3つの構成要素で成り立たせる事が一般的かつ標準的な利用方法かと思います。
しかしながら、状況によってはテンプレート部をJSで動的に記述したいというシチュエーションも出てくると思います。
例えばロジックは共通だが、テンプレートだけ差し替えて見た目の制御をおこないたいといった場合、
又は、バックエンドからHTMLのコードを受信してレンダリングさせたいがv-htmlを利用するとXSSのリスクがあるので、
Vueにレンダリングされるより前にVueのテンプレートとして動的にテンプレートを記述したい。
といった場合などが例であると考えます。
以下のような関数を用いる事で、動的にコンポーネントを生成する事が可能となります。
※.以下のサンプルコードはVue2でComposition-apiを利用しているサンプルとしてimportで@vue/composition-apiを読み込んでおります。
// dynamicComponentGenerator.ts
<script type="ts">
import { defineComponent, PropType, Ref, ref, computed } from '@vue/composition-api'
import { Component } from 'vue'
type ClickableText = {
text: string
clickHandler: ((event: Event) => void) | null
}
type SetupReturnType = {
clickableText: Ref<ClickableText>
}
type LinkText = {
text: string
}
const props = {
linkText: {
type: Object as PropType<LinkText>,
required: false,
default: '',
},
}
export const dynamicComponentGenerator = (componentName: string, template: string): Component =>
defineComponent({
componentName,
props,
emits: ['on-dynamic-text-clicked'],
setup(props, { emit }): SetupReturnType {
const clickableText = computed(() => {
const obj: ClickableText = { text: '', clickHandler: null}
if( props.linkText?.text !== '' ){
obj.text = props.linkText?.text
obj.clickHandler = emit('on-dynamic-text-clicked')
}
return obj
}
return {
clickableText,
}
}
template,
})
</script>
このような感じで
戻り値の型をComponentとして引数にcomponentName(コンポーネント名)とtemplate(動的に生成したテンプレートの文字列)を受け取る事で
動的コンポーネントを生成する事が可能となります。
この例ではpropsで親コンポーネントからリンク用のlinkTextを設定して渡せば
それがクリッカブルになり、固定のemitが親コンポーネントへ発火されます。
ここでは固定処理にしていますがpropsで渡された内容に応じて発火イベントとなるemitを新たに設定して
切り替えても良いですし、emitにパラメータを渡して処理を分岐したりしても良いかと思います。
※). ここでpropsのlinkTextをStringではなく、Objectにしているのは深い意味はありません。Stringでも何ら問題ありません。
定義だけだと分かりにくいかと思いますので実際に関数を呼び出して、動的コンポーネントを利用してみましょう。
// index.vue
<template>
<div>
<component :is="getLinkComponent(true)" @on-dynamic-text-clicked="linkClick" />
<component :is="getLinkComponent(false)" />
</div>
</template>
<script type="ts">
import { defineComponent } from '@vue/composition-api'
import { dynamicComponentGenerator } from './dynamicComponentGenerator.ts'
import { Component } from 'vue'
const LinkComponent = dynamicComponentGenerator(
'LinkComponent',
'<span><a @click="clickableText.clickHandler">{{clickableText.text}}</a></span>'
)
const NoNLinkComponent = dynamicComponentGenerator(
'NoNLinkComponent',
'<span>{{clickableText.text}}</span>'
)
export default defineComponent({
name: 'loadDynamicComponent',
setup() {
return {
getLinkComponent: (isActive: boolean): Component {
if( isActive ) return LinkComponent
return NoNLinkComponent
},
linkClick: () {
console.log('動的に生成されたリンクがクリックされました。')
}
}
}
})
</script>
実用レベルの例にすると複雑化して理解しにくくなると考え、出来る限りシンプルに例示したので
正直このサンプルそのものは何の役にも立たないサンプルだと思いますが
このサンプルに動的生成の全ての基本が詰まっていると考えています。
ポイントはsetupの中でreturnしているgetLinkComponentというメソッドですが、このメソッドはコンポーネントを生成する関数(dynamicComponentGenerator)を実行して生成されたコンポーネントのインスタンスを戻り値として返却しているのが注視すべき点になるかと思います。
(※.コンポーネント名ではない事に注意)
更に言及すると、dynamicComponentGeneratorの実行部分の第2引数で記述されているStringの中の
clickable.clickHandlerとclickHandler.textはdynamicComponentGenerator.tsで定義されている
dynamicComponentGeneratorが返却するコンポーネントのsetup関数の戻り値になります。
これらのテキストが解釈されるのは、Vueによりテンプレートがレンダリングされた後になり
どのタイミングでどの部分がどのように解釈されるのかが一見すると理解しづらいのですが、
このコードを理解する上で重要になるので、その点に着目してコードを読み返していただけるとよいかと思います。