この記事は、Vue #2 Advent Calendar 2019 の6日目の記事です。
はじめに
すでにグローバルに登録されているコンポーネントに対して、独自のスタイルを当てたり、特定のprops
に固定の値を流したりして再利用したい場合のやり方です。
主にライブラリやフレームワークで提供されているコンポーネントに対して、プロジェクトで統一したスタイルや動きを反映させたい時に使われるイメージです。
説明ではTypeScript
、vue-property-decorator
、Pug
、Vuetify
を使用しています。
仕様例
今回の説明のための、簡単な仕様です。
- Vuetifyのv-selectを拡張する
- 常に
color
をred
とする - ほかの
props
はすべてv-select
に準じる
素直にprops
とslot
を記述する
まずは普通に記述するパターン。
<template lang="pug">
v-select(v-bind="$props" color="red")
template(v-slot:append)
slot(name="append")
template(v-slot:default)
slot
//- ...
//- 多いので省略
</template>
<script lang="ts">
import Vue from "vue";
import { Component, Prop } from "vue-property-decorator";
@Component({})
export default MySelect class extends Vue {
@Prop() appendIcon: string;
@Prop() appendOuterIcon: string;
@Prop() attach: any;
@Prop({ type: Boolean, default: false }) autofocus: boolean;
// ...
// 多いので省略
}
</script>
<style scoped>
/* 独自のスタイル */
</style>
出来なくはありませんが、v-select
で定義されているすべてのprops
とslot
を用意しなければならないので、非常に冗長です。
$attrs
と描画関数(render
)を使う
こちらが本題の、$attrs
とrender
関数を使って受け取ったものをすべてv-select
に流してしまうパターンです。
<script lang="ts">
import Vue, { CreateElement } from "vue";
import { Component, Prop } from "vue-property-decorator";
@Component({
inheritAttrs: false
})
export default MyRenderSelect class extends Vue {
// 独自に受け取りたいpropsだけ定義
@Prop({ type: Boolean, default: false }) hoge: boolean;
render(h: CreateElement) {
const children = Object.keys(this.$slots).map(slot =>
h("template", { slot }, this.$slots[slot])
);
return h("v-select", {
class: {
"my-select--hoge": this.hoge,
},
props: {
...this.$attrs,
...this.$props,
color: "red",
},
on: this.$listeners,
scopedSlots: this.$scopedSlots
}, children);
}
}
</script>
<style scoped>
/* 独自のスタイル */
</style>
必要最低限の記述だけで、v-select
を使いながら、色を指定することができました。
ここでポイントとなるのは、以下の2点です。
-
inheritAttrs
と$attrs
-
slot
の渡し方
inheritAttrs
と$attrs
inheritAttrs
は2.4.0から追加されたオプションで、プロパティとして定義されていないバインディングがどのように扱われるのかを制御できるものです。
以下公式の説明です。
デフォルトでは、親スコープのバインディングはプロパティとして認識されず”フォールスロー”され、子コンポーネントのルート要素に通常の HTML 属性として適用されます。ターゲット要素または別のコンポーネントをラップ (wrap) するコンポーネントを著作する場合は、これは常に望ましい動作ではないかもしれません。
inheritAttrs
にfalse
を設定することで、このデフォルトの動作を無効にできます。属性は、$attrs
インスタンスプロパティ (2.4 での新規) を介して利用でき、v-bind
を使用してルートではない要素に明示的にバインドできます。注意: このオプションは
class
とstyle
のバインディングには効果がありません。
より具体的な動きはこちらの記事が参考になると思います。
Vue.js inheritAttrsの挙動を調べる - http://tic40.hatenablog.com/entry/2018/07/25/080000
今回のMyRenderSelect
はv-select
で使用するプロパティを定義していません。
そのため、デフォルトの状態ではv-select
のHTML属性として適用されてしまいます。
my-render-select(:items="['hoge', 'fuga']")
これを回避するために、inheritAttrs
をfalse
にしています。
また、受け取った属性(v-select
に渡すもの)は$attrs
から取り出すことができるので、そのままv-select
のprops
に渡してあげます。
slotの渡し方
次に、スロットの渡し方ですが、$slots
から取り出して、描画関数を用いてVNodeを構築します。
$slots
の公式の説明です。
プログラム的にスロットにより配信されたコンテンツにアクセスするために使用されます。各名前付きスロット は自身に対応するプロパティを持ちます (例:
v-slot:foo
のコンテンツはvm.$slots.foo
で見つかります)。default
プロパティは、名前付きスロットに含まれない任意のノード、またはv-slot:default
のコンテンツを含みます。注意:
v-slot:foo
は 2.6 以降でサポートされます。古いバージョンでは、非推奨の構文 を利用できます。
vm.$slots
のアクセスは、描画関数 によるコンポーネントを書くときに最も便利です。
$slots
の型は{ [name: string]: ?Array<VNode> }
でキーにスロット名、値にスロットのVNodeが入っているので、すべてのスロットについて、template
を使用したVNodeを構築します。
const children = Object.keys(this.$slots).map(slot =>
h("template", { slot }, this.$slots[slot])
);
これをv-slot
の子要素として渡してあげることで、1つ1つスロットを定義しなくてもv-slot
にスロットを渡すことができます。
ちなみに、scopedSlots
は$scopedSlots
を渡すだけで問題ありません。
まとめ
すでにグローバルに登録されているコンポーネントの拡張方法について紹介しました。
望ましいのはライブラリ等を使わずに、プロジェクトのスタイルガイドに沿ったコンポーネントを作成することだと思いますが、現実そこまで工数を割けないことも多いかと思います。(ライブラリが良く出来ているとなおさら)
そんな時に少しでもこの記事がお役に立てれば幸いです。
次の記事
明日は @hex2323 さんの Laravel + Vue.jsプロジェクト導入前の3つの心得です!