2018-11-04
Vue.jsのScoped Slotについて。
まず、Vue.jsにはスロットという機能があり、<slot></slot>
の位置に任意のテンプレートを当てはめることができる。
<a v-bind:href="url">
<slot></slot>
</a>
例として、<navigation-link>
コンポーネントを定義
<navigation-link url="/profile">
Your Profile
</navigation-link>
親コンポーネント内で、<navigation-link>
コンポーネントを使用。
<a v-bind:href="url">
Your Profile
</a>
レンダリング結果。slot
の部分に「Your Profile」の文字が入っていることがわかる。
以上が基本的なスロットの使い方。
スコープ付きスロット(Scoped Slot)を使うことで、子コンポーネントから親コンポーネントにプロパティを渡すことができる。
スコープ付きスロットをうまく使うことで、子コンポーネントで振る舞いを定義し、親コンポーネントから見た目を定義するといった分担ができる。結果として再利用性の高いコンポーネントを作り出すことが可能となる。
サンプルとして簡単なToDoリストを作って試してみた。
xボタンをクリックするとToDoの文字が薄くなる。それだけ!
このサンプルの登場人物は、スロットを持つ ToDo.vue
コンポーネントと、それを使用する親コンポーネントの App.vue
の2つ。
ToDoコンポーネントはデータとメソッドを1つずつ持っている。
todos
): todoのタイトルと完了状況のオブジェクトを持つ配列toggleDone
): 個別のtodoの完了状況を切り替える<template>
<div>
<!-- データとメソッドをスロットにバインディング -->
<slot :todos="todos" :toggle="toggleDone"></slot>
</div>
</template>
<script>
export default {
name: "ToDo",
data() {
return {
todos: [
{title: 'ブログ書く(1本目)', isDone: false},
{title: 'ブログ書く(2本目)', isDone: false},
{title: 'ブログ書く(3本目)', isDone: false},
{title: 'ブログ書く(4本目)', isDone: false}
]
};
},
methods: {
toggleDone (todo) {
todo.isDone = !todo.isDone
}
}
};
</script>
データとメソッドをスロットにバインドするだけで、親コンポーネントからアクセスする準備は完了。
App.vueのtemplate部分だけ抜粋。
<to-do></to-do>
の内側でToDo.vueのデータとメソッドにアクセスしている。(slot-scope="{todos, toggle}"
の部分)
<template>
<div id="app">
<to-do>
<template slot-scope="{todos, toggle}">
<!-- todos(配列)に対してfor文 -->
<div v-for="todo in todos" :key="todo.title">
<!-- 配列の各要素はオブジェクト {title: string, isDone: boolean} -->
<span :style="todo.isDone ? {opacity: 0.2} : {}">
{{todo.title}}
</span>
<!-- ボタンクリックでtoggleDone()を実行 -->
<button @click="toggle(todo)">×</button>
</div>
</template>
</to-do>
</div>
</template>
このような感じで、子コンポーネントから受け取ったプロパティをもとにスロット内に当てはめるテンプレートを自由に書くことができる。
今回のサンプルでは、子コンポーネントはあくまでデータとメソッドを提供しているに過ぎないので、親コンポーネントから自由に見た目を変更することができる。
例えば以下のように変更すれば「ボタンは無し、todoのテキストを直接クリックで完了状態を切り替え」というコンポーネントになる。振る舞いを簡単に使い回せるので、とてもお手軽。
<template>
<div id="app">
<to-do>
<template slot-scope="{todos, toggle}">
<div v-for="todo in todos" :key="todo.title">
<span
@click="toggle(todo)"
:style="todo.isDone ? {opacity: 0.2} : {}"
>{{todo.title}}</span>
</div>
</template>
</to-do>
</div>
</template>
一見ややこしさはあるが、使いこなせばかなり便利。特に「動作は同じだがレイアウトが違う」というコンポーネントが複数あるとき、スコープ付きスロットは真価を発揮できる。
スコープ付きスロットを突き詰めていくことで、全くビューを持たない「Renderless Components」にたどり着くということなので、それもあわせて可能性を探っていきたい。