7.1 子コンポーネントをカスタマイズする Slot
7.1.1 Slot とは
親コンポーネントから子コンポーネントにデータを渡すには Props が利用できる。
ただし Props では HTML 要素そのものを渡すことができない。
const tag = ref("<p>連絡がつきません。</p>") // テンプレート変数 tag を用意
:
<One Section v-bind:tag="tag" /> <!-- Props 経由で子コンポーネントに渡す -->
<section class="box">
{{tag}} <!-- マスタッシュ構文で表示 -->
</section>
マスタッシュ構文で表示しようとした場合、エスケープされていしまうためHTML要素として認識されず、あくまでタグ形式の文字列として表示されてしまう。
<p> 連絡がつきません。</p>
v-htmlディレクティブを利用すればHTML要素としてレンダリングされるが、XSS脆弱性がふくまれてしまう。
<section class="box" v-html="tag"> <!-- v-htmlディレクティブを使うとレンダリングされる -->
</section> <!-- 脆弱性がふくまれる -->
さらに v-html ディレクティブを使ったとしても Props で子コンポーネントに渡すことができるのは性的な HTML 記述が基本なので v-for によって動的にレンダリングされた HTML 要素を渡すことはできない。
<ul>
<li v-for="problem in problems" v-bind:key="prpblem">
{{problem}}
</li>
</ul>
静的であれ動的であれ HTML要素を子コンポーネントに渡して子コンポーネントでそのままレンダリングさせるには、子コンポーネントのレンダリングの内容を親コンポーネントがカスタマイズできるような別の仕組みが必要になる。この仕組みが Slot(スロット)である。
7.1.2 Slot の基本的な記述方法
<script setup lang="ts">
interface Props {
name: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h1>{{ name }}さんの状況</h1>
<!-- 1. slotタグ-->
<slot />
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const taro = ref('田中太郎')
</script>
<template>
<section>
<h2>Slotの利用</h2>
<OneSection v-bind:name="taro">
<!-- 子コンポーネントタグの要素としてHTMLを記述-->
<p>連絡がつきません。</p>
</OneSection>
</section>
</template>
OneSection.vue
- 親コンポーネントから渡されたHTML要素を子コンポーネントで Slot を利用して表示するには slotタグを使う。
slot タグの位置に親コンポーネントから渡された HTML 要素がレンダリングされる。
App.vue
- 子コンポーネントに親コンポーネントから HTML 要素を渡すには、親コンポーネント側で子コンポーネントタグの要素として HTML を記述する。
結果
<section>
<h2>Slotの利用</h2>
<section class="box">
<h1>田中太郎さんの状況</h1>
<!-- slotタグ-->
<!-- 子コンポーネントタグの要素としてHTMLを記述-->
<p>連絡がつきません。</p>
</section>
</section>
7.1.3 Slot のフォールバックコンテンツ
子コンポーネントの Slot に対して親コンポーネントから必ず HTML 要素が渡される保証はない。
HTML要素が渡されなかった場合を想定してデフォルトの HTML 要素を設定することができる。
これをフォールバックコンテンツ(Fallback Content)という。
<script setup lang="ts">
interface Props {
name: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h1>{{ name }}さんの状況</h1>
<slot>
<!-- 1. -->
<p>問題ありません。</p>
</slot>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const taro = ref('田中太郎')
const jiro = ref('鈴木二郎')
</script>
<template>
<section>
<h2>Slotの利用</h2>
<!-- 1. -->
<OneSection v-bind:name="taro">
<p>連絡がつきません。</p>
</OneSection>
<!-- 2. -->
<OneSection v-bind:name="jiro" />
</section>
</template>
App.vue
- HTML 要素を記載して子コンポーネントを読み込んでいる。
- HTML 要素を記載せずに子コンポーネントを読み込んでいる。
OneSection.vue
- slot タグを開始タグと終了タブに分離し、その間に HTML 要素を記述することでフォールバックコンテンツとなる。
結果
<section>
<h2>Slotの利用</h2>
<!-- 1. -->
<section class="box">
<h1>田中太郎さんの状況</h1>
<p>連絡がつきません。</p>
</section>
<!-- 2. -->
<section class="box">
<h1>鈴木二郎さんの状況</h1>
<p>問題ありません。</p> <!-- HTML 要素が渡されていないのでデフォルト -->
</section>
</section>
7.1.4 テンプレート変数の所属先
親コンポーネントから子コンポーネントに渡す HTML 要素内でテンプレート変数を利用する場合、その変数は原則親コンポーネントで用意する。
<script setup lang="ts">
interface Props {
name: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h1>{{ name }}さんの状況</h1>
<slot>
<!-- 1. -->
<p>{{ name }}さんは問題ありません。</p>
</slot>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const taro = ref('田中太郎')
const jiro = ref('鈴木二郎')
</script>
<template>
<section>
<h2>Slotの利用</h2>
<!-- 1. -->
<OneSection v-bind:name="taro">
<p>{{ taro }}さんは連絡がつきません。</p>
</OneSection>
<!-- 2. -->
<OneSection v-bind:name="jiro" />
</section>
</template>
App.vue
- OneSection タグ内に Slot コードとしてHTML要素を記述し、そこでテンプレート変数 taro を利用している。
OneSection に HTML要素が渡される時点でテンプレート変数は展開され「<p>田中太郎さんは連絡が付きません。</p>」というHTML要素が生成され渡される。
7.1.5 親でのレンダリング結果の Slot
親コンポーネントで動的にレンダリングした HTML 要素を子コンポーネントに渡す。
<script setup lang="ts">
interface Props {
name: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h1>{{ name }}さんの状況</h1>
<slot>
<p>問題ありません。</p>
</slot>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const taroProblemsInit: string[] = ['電話が通じません。', '留守です。']
const taroProblems = ref(taroProblemsInit)
const taro = ref('田中太郎')
const jiro = ref('鈴木二郎')
</script>
<template>
<section>
<OneSection v-bind:name="taro">
<ul>
<li v-for="problem in taroProblems" v-bind:key="problem">
{{ problem }}
</li>
</ul>
</OneSection>
<OneSection v-bind:name="jiro" />
</section>
</template>
Slot に挿入される HTML 要素は ul 要素であり、内容は v-for でループ処理しながら生成している。
親コンポーネントである App 内で、App 内のテンプレート変数を利用して子テンプレートに渡す HTML 要素を生成したうえで、それを Slot として渡している。
Slot を利用すればこのような柔軟な表示も可能である。
7.2 複数の Slot を実現する名前付き Slot
7.2.1 slot タグの追加
子コンポーネントに複数の slot タグを記述したとしても、それらには全て同じ HTML 要素が挿入される。
<section class="box">
<h1>{{ name }}さんの状況</h1>
<slot/>
<slot/>
</section>
Slotの利用
田中太郎さんの状況
連絡がつきません。←1つ目の<slot/>
連絡がつきません。←2つ目の<slot/>
7.2.2 名前付き Slot
ひとつのコンポーネントの複数の個所に異なる HTML 要素を挿入したい場合、それぞれの slot タグに名前をつけることになる。これを名前付き Slot(Named Slots)という。
<script setup lang="ts">
interface Props {
name: string
}
defineProps<Props>()
</script>
<template>
<section class="box">
<h1>{{ name }}さんの状況</h1>
<slot> <!-- 1. -->
<p>問題ありません。</p>
</slot>
<h4>詳細内容</h4>
<slot name="detail"> <!-- 2. -->
<p>とくにありません。</p>
</slot>
</section>
</template>
<style>
.box {
border: green 1px solid;
margin: 10px;
}
</style>
<script setup lang="ts">
import { ref } from 'vue'
import OneSection from './components/OneSection.vue'
const taroProblemsInit: string[] = ['電話が通じません。', '留守です。']
const taroProblems = ref(taroProblemsInit)
const taro = ref('田中太郎')
const jiro = ref('鈴木二郎')
</script>
<template>
<section>
<OneSection v-bind:name="taro"> <!-- 1. -->
<template v-slot:default> <!-- 1-1. -->
<p>問題発生</p>
</template>
<template v-slot:detail> <!-- 1-2. -->
<ul>
<li v-for="problem in taroProblems" v-bind:key="problem">
{{ problem }}
</li>
</ul>
</template>
</OneSection>
<OneSection v-bind:name="jiro" /> <!-- 2. -->
<OneSection v-bind:name="jiro"> <!-- 3. -->
<template v-slot:default>
<p>微妙な問題発生</p>
</template>
</section>
</template>
OneSection.vue
- これまで記述してきた通常の slot タグ
- name 属性として detail が記述されている名前付き Slot
7.2.3 名前付き Slot に HTML 要素を渡す v-slot
App.vue
slot タグに HTML 要素を渡す親コンポーネントではそれぞれのHTML要素を template タグで囲み、その上でどの slot タグに挿入するかを表す v-slot ディレクティブを記述する。
<!-- 名前のないSlot -->
<template v-slot:default>
:
</template>
<!-- 名前を指定 -->
<template v-slot:名前>
:
</template>
<!-- v-slot の省略形(非推奨) -->
<template #名前>
:
</template>
- 名前なし(default)v-slot と 名前付き v-slot
- 名前なし slot タグにレンダリングされる
- name 属性が detail の slot タグにレンダリングされる
- v-slot なし
これまで通りそれぞれの slot タグ内に記述したフォールバックコンテンツがレンダリングされる - 名前なし(default)v-slot のみ
全ての Slot に対して子コンポーネントに渡す HTML 要素を記述する必要はない。
ここでは、名前なし(default)Slot に入れる HTML 要素だけを子コンポーネントにわたしているので、detail という名前の Slot ではフォールバックコンテンツがレンダリングされる。
v-slot で指定する挿入先 Slot 名としてテンプレート変数を使うことも可能である。
<OneSection v-bind:name="jiro">
<template v-slot:[SlotName]>
<p>微妙な問題発生</p>
</template>
</OneSection>
v-slot:[テンプレート変数名]
7.3 データの受け渡しを逆転させるスコープ付き Slot
7.3.1 スコープ付き Slot とは
コンポーネント内の変数は処理はコンポーネント内で閉じておくのが原則であり、Slotでも例外ではない。
しかし Slot にはこの原則から外れる親コンポーネントから子コンポーネントの変数を利用するための仕組みが設けられている。
これをスコープ付きSlot(Scoped Slots)という。
<script setup lang="ts">
import { reactive } from 'vue'
const memberInfo = reactive({ // 1.
name: '田中太郎',
state: '問題ありません。',
})
</script>
<template>
<section>
<slot v-bind:memberInfo="memberInfo"> <!-- 2. -->
<h1>{{ memberInfo.name }}さんの状況</h1> <!-- 3. -->
<p>{{ memberInfo.state }}</p> <!-- 3. -->
</slot>
</section>
</template>
<script setup lang="ts">
import OneSection from './components/OneSection.vue'
</script>
<template>
<section>
<OneSection>
<template v-slot:default="slotProps"> <!-- 1. -->
<dl>
<dt>名前</dt>
<dd>{{ slotProps.memberInfo.name }}</dd> <!-- 2. -->
<dt>状況</dt>
<dd>{{ slotProps.memberInfo.state }}</dd> <!-- 3. -->
</dl>
</template>
</OneSection>
</section>
</template>
名前
田中太郎
状況
問題ありません。
7.3.2 子から親へデータを渡す Slot Props
OneSection.vue
- 子コンポーネントでデータを用意
- 子コンポーネントから Slot 経由で親コンポーネントにデータを渡している
このような方法を Slot Props(ストっとプロップス)という。 - スコープ付き Slot でもフォールバックコンテンツはそのまま利用できる。
親コンポーネントで要素を持たない OneSection タグを記述した場合、フォールバックコンテンツが表示される。
<slot v-bind:Slot Prop名="該当データのテンプレート変数名">
例ではデータを memberInfo オブジェクトにまとめて Slot Props としているが、これらをバラバラにして各々のデータを Slot Props としても問題はない。
その場合は複数の v-bind ディレクティブを記述する。
<slot
v-bind:memberName="memberInfo.name"
v-bind:memberState="memberInfo.state">
7.3.3 Slot Props の受け取り方法
App.vue
- v-slot ディレクティブの属性値を利用して Slot Props として子コンポーネントから渡されたデータを親コンポーネントで受け取っている。
- スコープ付き Slot では名前付き Slot を利用していなくても必ず template タグを利用する。
- template タグ内に v-slot ディレクティブを記述する。
- 子コンポーネントでの記述が名前なし slot タグのみの場合は v-slot ディレクティブの引数を default とする。
- v-slot ディレクティブに属性値を設定する。名前は任意(慣習的に slotPropsが多い)
- 属性値で指定したオブジェクトに Slot Props の全データが含まれる。
- memberInfo オブジェクトを取り出し name データを表示
- memberInfo オブジェクトを取り出し state データを表示
<template v-slot:名前またはdefault="属性値(慣習的に slotProps)">
JavaScript/TypeScript には分割代入という仕組みがあり、 Slot Props でこの仕組みを利用することができる。
<template v-slot="{memberInfo}">
<dl>
<dt>名前</dt>
<dd>{{memberInfo.name}}</dd>
<dt>状況</dt>
<dd>{{memberInfo.state}}</dd>
</dl>
</template>
{} によってオブジェクト内の指定プロパティ、この場合なら memberInfo のみを受け取ることができる。
最終的にこれらのデータを利用してレンダリングされた dl 要素がもう一度子コンポーネントである OneSection の slot タグ内に挿入されて表示される。これがスコープ付き Slot の処理の流れである。
<!-- バラバラのSlot Props -->
<slot
v-bind:memberName="memberInfo.name"
v-bind:memberState="memberInfo.state">
<dt>名前</dt>
<dd>{{slotProps.memberName}}</dd>
<dt>状況</dt>
<dd>{{slotProps.memberState}}</dd>
<template v-slot="{memberName, memberState}">
<dl>
<dt>名前</dt>
<dd>{{memberName}}</dd>
<dt>状況</dt>
<dd>{{memberState}}</dd>
</dl>
<template v-slot:default="slotProps">
:
</template>
<template v-slot:detail="detailSlotProps">
:
</template>
7.4 動的コンポーネント
7.4.1 動的コンポーネントとは
これまで名指しで指定していた子コンポーネントをテンプレート変数で指定して動的に変更する仕組みが動的コンポーネント(Dynamic Components)である。
<script setup lang="ts">
import { ref } from 'vue'
const inputNameModel = ref('田中太郎')
</script>
<template>
<input type="text" v-model="inputNameModel" />
<p>{{ inputNameModel }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const memberType = ref(1)
</script>
<template>
<label>
<input type="radio" name="memberType" value="1" v-model="memberType" />
通常会員
</label>
<label>
<input type="radio" name="memberType" value="2" v-model="memberType" />
特別会員
</label>
<label>
<input type="radio" name="memberType" value="3" v-model="memberType" />
優良会員
</label>
<br />
<p>選択されたラジオボタン: {{ memberType }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const memberTypeSelect = ref(1)
</script>
<template>
<select v-model="memberTypeSelect">
<option value="1">通常会員</option>
<option value="2">特別会員</option>
<option value="3">優良会員</option>
</select>
<br />
<p>選択されたリスト: {{ memberTypeSelect }}</p>
</template>
<script setup lang="ts">
import { shallowRef, ref } from 'vue'
import Input from './components/InputName.vue' // 1.
import Radio from './components/RadioMember.vue' // 1.
import Select from './components/SelectType.vue' // 1.
// 現在表示させるコンポーネントを表すテンプレート変数
const currentComp = shallowRef(Input) // 2.
// 現在表示させるコンポーネント名を表すテンプレート変数
const currentCompName = ref('Input') // 3.
// コンポーネントの配列
const compList = [Input, Radio, Select] // 4.
const compNameList: string[] = ['Input', 'Radio', 'Select'] // 5.
// 現在表示させているコンポーネントに対応した配列のインデックス番号
let currentCompIndex = 0
// コンポーネントを切り替えるメソッド
const switchComp = (): void => { // 6.
// インデックス番号をインクリメント
currentCompIndex++
// インデックス番号が3以上なら
if (currentCompIndex >= 3) {
// 0にリセット
currentCompIndex = 0
}
// インデックス番号に該当するコンポーネントを currentCompに代入
currentComp.value = compList[currentCompIndex]
// インデックス番号に該当するコンポーネント名を currentCompName に代入
currentCompName.value = compNameList[currentCompIndex]
}
</script>
<template>
<p>コンポーネント名:{{ currentCompName }}</p>
<keep-alive> // 7.
<component v-bind:is="currentComp" /> // 8.
</keep-alive>
<button v-on:click="switchComp">切り替え</button>
</template>
7.4.2 componet タグと v-bind:is
App.vue
<component v-bind:is="コンポーネントを格納したテンプレート変数"/>
これまではコンポーネントを読み込む部分にコンポーネント名そのもののタグを記述していた。
動的コンポーネントでは代わりに component タグを使って v-bind:is ティレクティブを記述する。
このディレクティブの属性値は読み込むコンポーネントオブジェクトを表すテンプレート変数で、この変数に該当するコンポーネントが読み込まれる仕組みであるため、変数の値を返送すれば読み込まれるコンポーネントが動的に変更される。
- 用意した子コンポーネントをインポート
- コンポーネントオブジェクトを格納するテンプレート変数を用意
Inputオブジェクトで初期化 - コンポーネント名を表すテンプレート変数を用意
“Input”で初期化 - コンポーネントオブジェクトを要素とする配列を用意
- コンポーネント名の配列を用意
- コンポーネント切り替えメソッド
コンポーネントオブジェクトと同時にコンポーネント名の切り替えを行う - 動的にレンダリングされたコンポーネントの状態を保持する働きのタグ
- 動的コンポーネント
コメント