😎

Vue2で孫コンポーネントにprops, events, slotsをリレーする

2022/08/04に公開

始めに

コンポーネントをラップして少しだけ拡張させたコンポーネントを作るケースが度々あると思いますが、そのときにprops, events, slotsを再定義するのは大変な上、元々のコンポーネントに修正が入ったら修正を追従する必要があります。その辺を楽にするために、受け取ったpropsなどをそのままリレーするような書き方が実はあるのですが、その情報がまとまっていなさそうなのでまとめてみました。

例としてv-data-tableに検索ボックスを合体させてローカルで検索するdata-table-with-searchコンポーネントを作る話にしたいと思います。

それぞれのリレー方法

propsをリレーする

propsを定義しなかったデータは$attrsに入ってくるので、それをv-bindで全部流し込むと送ることができます。

<template>
  <v-data-table
    v-bind="$attrs"
  >
  </v-data-table>
</template>

<script>
export default Vue.extends({
  inheritAttrs: false,
});
</script>

attributesとして認識されたものは何もしないとDOMに表示されてデバッグの邪魔になるので、inheritAttrs: falseにして表示されないようにします。

参考

https://jp.vuejs.org/v2/guide/components-props.html#プロパティでない属性

https://qiita.com/mpyw/items/d3dc2745b6808b0158c3

eventsをリレーする

eventの設定は簡単で、$listeners@input="~"みたいに書いた内容を全部受け取ってくれるので、これをv-onに入れてあげます。

 <template>
   <v-data-table
     v-bind="$attrs"
+    v-on="$listeners"
   >
   </v-data-table>
 </template>

参考

https://jp.vuejs.org/v2/api/#vm-listeners

slotsをリレーする

slotsは$scopedSlotsに入ってくるので、これをループして改めてslotを定義します。

 <template>
   <v-data-table
     v-bind="$attrs"
     v-on="$listeners"
   >
+    <template v-for="(_, slot) in $scopedSlots" v-slot:[slot]="props">
+     <slot :name="slot" v-bind="props" />
+    </template>
   </v-data-table>
 </template>
<!-- 親コンポーネントから呼び出し -->
<template>
  <data-table-with-search>
    <template v-slot:[`item.sex`]="{ item }">
      <v-chip
        :color="item.sex === '男性' : 'blue' : 'red'"
	label
	dark
      >{{ item.sex }}</v-chip>
    </template>
    <template v-slot:[`item.url`]="{ item }">
      <a
        :href="item.url"
        target="_blank"
        rel="noreferrer noopener"
      >{{ item.url }}</a>
    </template>
  </data-table-with-search>
</template>

<!-- DataTableWithSearchコンポーネントは以下のように展開される -->
<template>
  <v-data-table
    v-bind="$attrs"
    v-on="$listeners"
  >
    <!--
      v-data-tableにある`item.sex`, `item.url`slot情報を受け取って、
      改めてこのコンポーネントでも`item.sex`, `item.url`slotを使えるように再定義
    -->
    <template v-slot:[`item.sex`]="props">
     <slot :name="slot" v-bind="props" />
    </template>
    <template v-slot:[`item.url`]="props">
     <slot :name="slot" v-bind="props" />
    </template>
  </v-data-table>
</template>

補足

今回v-slot:topに検索ボックスを入れることにしましたが、$scopedSlotsの方でも展開されてしまったらどうなるのかも検証してみました。結論を言うと、後に定義された方が優先されるようで、上書きさせたいかどうかで順番を決めると良いと思いました。

<template>
  <v-data-table
    v-bind="$attrs"
    :search="$data.searchWord"
    v-on="$listeners"
  >
    <!-- 先にv-slot:topを拡張定義する -->
    <template v-slot:top>
      <v-text-field
        v-model="searchWord"
        label="Search"
        outlined
        dense
        hide-details
      />
    </template>
    <!--
      $scopedSlotsでもv-slot:topが定義される可能性があり、
      定義された場合は後勝ちでこちらが優先される
      (上書きされたくない場合は$scopedSlotsのループを先に書く)
    -->
    <template v-for="(_, slot) in $scopedSlots" v-slot:[slot]="props">
      <slot :name="slot" v-bind="props" />
    </template>
  </v-data-table>
</template>

参考

https://stackoverflow.com/a/53431262

余談

Vue2には$slots$scopedSlotsがあるのですが、$slotsの方はv-slot:scope_name="props"みたいに値を受け取っちゃうと拾えなくなってしまうようです。
https://stackoverflow.com/questions/63756681/why-vue-regular-slots-also-available-in-this-scopedslots

終わりに

以上がVue2で孫コンポーネントにpropsなどをリレーさせる方法でした。Vue2は今更感な気はしますが、Vue3も似たようなことをすればリレーできるはずなのでご参考になれれば幸いです。
完成版のサンプルは以下に置きますので、興味がある方は是非みてください。

Discussion