🐈

Vue3のコンポーネントの「フォールスルー属性」、属性・プロパティの継承

2024/04/03に公開

作りたかったもの


今回VueのUIフレームワーク「Vuetify」を使用しました。
Vuetifyを使用したうえで、上画像のような、マウスを重ねると色が変わり、Tipが表示されるようなボタンが欲しかったので、VuetifyのVBtnを内包した新しいコンポーネント(ここではIconBtn)を自作しました。

課題

作成したのはよかったのですが以下のようなエラーが出ました。

Extraneous non-props attributes were passed to component but could notbe automatically inherited because component renders fragment or text root nodes.

「class」や「density」属性が設定されてるけど無視されてる(適用されない)よー、みたいなエラーです。

この時のコード

このエラーが出た時のコードを以下に示します。

IconBtn.vue
<script lang="ts">
export default defineComponent({
  layout: false,
  name: "IconBtn",
  // inheritAttrs: false,
});
</script>
<script setup lang="ts">
interface Props {
  tips?: string,
  variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain",
  color?: string,
  icon: string,
  size?: string | number,
  disabled?: boolean,
  hover?: boolean,
}

const Props = withDefaults(defineProps<Props>(), {
  tips: undefined,
  variant: 'text',
  color: 'primary',
  icon: 'mdi-home',
  size: "default",
  disabled: false,
  hover: true,
});
</script>
<template>
  <v-hover #default="{ isHovering, props }">
    <v-btn v-bind="props" icon="" :variant="variant">
      <v-tooltip v-if="tips" activator="parent" :text="tips" location="top"/>
      <v-icon :icon="icon" :color="hover ? (isHovering ? color : 'gray') : color" :size="size"/>
    </v-btn>
  </v-hover>
</template>
index.vue
<template>
    <MyIconBtn icon="mdi-content-save" :color="dbUpdate ? 'error' : 'gray'" :hover="false" class="mx-2" density="compact"
                 :disabled="permission>3" @click="saveDB" tips="DB設計保存"/>
</template>

さて、何が起こっているのでしょうか。

「フォールスルー属性」とは

その前に、Vue3における「フォールスルー属性」についてです。
詳細は以下のページを参照していただければと思いますが、ざっくり説明しますと、コンポーネントの呼び出し(ここではindex.vueでの記述)時にコンポーネントに設定された属性のうち、子コンポーネント(ここではIconBtnコンポーネント)の「props」に含まれていない属性は、自動的に子コンポーネントのルート要素に継承される、というものです。
https://ja.vuejs.org/guide/components/attrs#fallthrough-attributes

つまり、例えば以下のような何も属性を持たないボタンコンポーネントを作成したとして、

MyBtn.vue
<template>
<button>BUTTON</button>
</template>

以下のように呼び出したとすると、

index.vue
<template>
<MyBtn @click="some" class="btn" :hoge="fuga"/>
</template>

実際にレンダリング(実質)されるのは、「@click」や「class」「hoge」を継承(フォールスルー)した以下のようなコンポーネントということになります。

<template>
<button @click="some" class="btn" :hoge="fuga">BUTTON</button>
</template>

原因

つまり、今回の事象の原因は、子コンポーネントのルート要素が「VBtn」ではなく、「hover」を検知するためのヘルパー的コンポーネント「VHover」であることです。
こいつは言ってしまえば実態が無いので、実際にレンダリングされるわけではなく、呼び出しもとで設定した「class」や「density」の受取先がない、ということなのです。

解決策

「$attrs」をbindして親で設定した属性を継承する

「$attrs」を使用することで、親コンポーネントで設定した属性にアクセスすることができます。
この「$attrs」を「v-bind」に設定することで、「フォールスルー属性」を全て任意の要素に継承できます。

<template>
<div>
  <v-btn v-bind="$attrs" icon="" :variant="variant">
    <v-tooltip v-if="tips" activator="parent" :text="tips" location="top"/>
    <v-icon :icon="icon" :color="hover ? (isHovering ? color : 'gray') : color" :size="size"/>
  </v-btn>
</div>
</template>

ただし、このままではルート要素である「div」にも「フォールスルー」が発生してしまうので、「inheritAttrs: false」を設定して止めます。

IconBtn.vue
<script lang="ts">
export default defineComponent({
  layout: false,
  name: "IconBtn",
  inheritAttrs: false,
});
</script>

以上です。

Vueは使いこなすには結構大変ですな。
Vueもきちんとドキュメントを読まなくてはいけないフレームワークになってきたようだ。

Discussion