[Vue.js] 7. 子コンポーネント利用のバリエーション

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

  1. 親コンポーネントから渡されたHTML要素を子コンポーネントで Slot を利用して表示するには slotタグを使う。
    slot タグの位置に親コンポーネントから渡された HTML 要素がレンダリングされる。

App.vue

  1. 子コンポーネントに親コンポーネントから 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

  1. HTML 要素を記載して子コンポーネントを読み込んでいる。
  2. HTML 要素を記載せずに子コンポーネントを読み込んでいる。

OneSection.vue

  1. 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

  1. 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

  1. これまで記述してきた通常の slot タグ
  2. 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>
  1. 名前なし(default)v-slot と 名前付き v-slot
    1. 名前なし slot タグにレンダリングされる
    2. name 属性が detail の slot タグにレンダリングされる
  2. v-slot なし
    これまで通りそれぞれの slot タグ内に記述したフォールバックコンテンツがレンダリングされる
  3. 名前なし(default)v-slot のみ
    全ての Slot に対して子コンポーネントに渡す HTML 要素を記述する必要はない。
    ここでは、名前なし(default)Slot に入れる HTML 要素だけを子コンポーネントにわたしているので、detail という名前の Slot ではフォールバックコンテンツがレンダリングされる。
名前付き 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

  1. 子コンポーネントでデータを用意
  2. 子コンポーネントから Slot 経由で親コンポーネントにデータを渡している
    このような方法を Slot Props(ストっとプロップス)という。
  3. スコープ付き 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

  1. v-slot ディレクティブの属性値を利用して Slot Props として子コンポーネントから渡されたデータを親コンポーネントで受け取っている。
    • スコープ付き Slot では名前付き Slot を利用していなくても必ず template タグを利用する。
    • template タグ内に v-slot ディレクティブを記述する。
    • 子コンポーネントでの記述が名前なし slot タグのみの場合は v-slot ディレクティブの引数を default とする。
    • v-slot ディレクティブに属性値を設定する。名前は任意(慣習的に slotPropsが多い)
    • 属性値で指定したオブジェクトに Slot Props の全データが含まれる。
  2. memberInfo オブジェクトを取り出し name データを表示
  3. memberInfo オブジェクトを取り出し state データを表示
<template v-slot:名前またはdefault="属性値(慣習的に slotProps)">
Slot Props の分割代入

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 の処理の流れである。

memberInfo がバラバラの Slot Props の場合
<!-- バラバラの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>
名前付き Slot がある場合
<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 ティレクティブを記述する。
このディレクティブの属性値は読み込むコンポーネントオブジェクトを表すテンプレート変数で、この変数に該当するコンポーネントが読み込まれる仕組みであるため、変数の値を返送すれば読み込まれるコンポーネントが動的に変更される。

  1. 用意した子コンポーネントをインポート
  2. コンポーネントオブジェクトを格納するテンプレート変数を用意
    Inputオブジェクトで初期化
  3. コンポーネント名を表すテンプレート変数を用意
    “Input”で初期化
  4. コンポーネントオブジェクトを要素とする配列を用意
  5. コンポーネント名の配列を用意
  6. コンポーネント切り替えメソッド
    コンポーネントオブジェクトと同時にコンポーネント名の切り替えを行う
  7. 動的にレンダリングされたコンポーネントの状態を保持する働きのタグ
  8. 動的コンポーネント

コメント

タイトルとURLをコピーしました