插槽让组件的内容更加灵活。父组件可以向子组件传递模板内容,而不仅仅是数据。这是 Vue 组件化开发中非常强大的功能。
假设你要创建一个按钮组件:
Vue.component('my-button', {
template: '<button class="btn"><slot>默认按钮</slot></button>'
})
没有插槽,按钮内容是固定的。有了插槽,可以这样使用:
<my-button>提交</my-button>
<my-button>取消</my-button>
<my-button><icon name="save"></icon> 保存</my-button>
同一个组件,展示不同的内容,这就是插槽的价值。
子组件使用 <slot> 标签定义插槽位置:
Vue.component('alert-box', {
template: `
<div class="alert">
<strong>提示:</strong>
<slot></slot>
</div>
`
})
父组件传入内容:
<alert-box>操作成功!</alert-box>
<alert-box>发生错误!</alert-box>
插槽可以指定默认内容,当父组件没有传入内容时显示:
Vue.component('submit-button', {
template: `
<button type="submit">
<slot>提交</slot>
</button>
`
})
<submit-button></submit-button> <!-- 显示:提交 -->
<submit-button>保存</submit-button> <!-- 显示:保存 -->
当组件需要多个插槽时,可以使用具名插槽。
Vue.component('base-layout', {
template: `
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
<base-layout>
<template v-slot:header>
<h1>页面标题</h1>
</template>
<p>主要内容</p>
<p>更多内容</p>
<template v-slot:footer>
<p>页脚信息</p>
</template>
</base-layout>
v-slot: 可以简写为 #:
<base-layout>
<template #header>
<h1>页面标题</h1>
</template>
<p>主要内容</p>
<template #footer>
<p>页脚信息</p>
</template>
</base-layout>
Vue 2.6.0 之前使用 slot 属性:
<!-- 已废弃,不推荐使用 -->
<base-layout>
<h1 slot="header">页面标题</h1>
<p>主要内容</p>
<p slot="footer">页脚信息</p>
</base-layout>
作用域插槽让父组件可以访问子组件的数据。
子组件通过 v-bind 传递数据:
Vue.component('user-list', {
props: ['users'],
template: `
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user">
{{ user.name }}
</slot>
</li>
</ul>
`
})
父组件通过 v-slot 接收数据:
<user-list :users="users">
<template v-slot="{ user }">
<span class="name">{{ user.name }}</span>
<span class="email">{{ user.email }}</span>
</template>
</user-list>
Vue.component('data-table', {
props: {
items: Array,
columns: Array
},
template: `
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :item="item" :value="item[col.key]">
{{ item[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
`
})
<data-table :items="users" :columns="columns">
<template #name="{ item }">
<strong>{{ item.name }}</strong>
</template>
<template #status="{ value }">
<span :class="'status-' + value">{{ value }}</span>
</template>
<template #actions="{ item }">
<button @click="edit(item)">编辑</button>
<button @click="delete(item)">删除</button>
</template>
</data-table>
Vue.component('card', {
props: {
title: String,
shadow: {
type: Boolean,
default: true
}
},
template: `
<div class="card" :class="{ 'card-shadow': shadow }">
<div v-if="$slots.header" class="card-header">
<slot name="header"></slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</div>
</div>
`
})
<card title="用户信息">
<template #header>
<h3>用户详情</h3>
</template>
<p>姓名:张三</p>
<p>邮箱:zhangsan@example.com</p>
<template #footer>
<button>编辑</button>
</template>
</card>
Vue.component('list', {
props: {
items: Array,
itemKey: {
type: String,
default: 'id'
}
},
template: `
<ul class="list">
<li v-for="item in items" :key="item[itemKey]" class="list-item">
<slot :item="item" :index="index">
{{ item }}
</slot>
</li>
</ul>
`
})
<list :items="products">
<template v-slot="{ item, index }">
<span class="index">{{ index + 1 }}</span>
<span class="name">{{ item.name }}</span>
<span class="price">¥{{ item.price }}</span>
</template>
</list>
Vue.component('tabs', {
template: `
<div class="tabs">
<div class="tabs-header">
<slot name="tab" v-for="tab in tabs" :tab="tab" :active="tab.active">
<button
:key="tab.name"
:class="{ active: tab.active }"
@click="selectTab(tab)"
>
{{ tab.label }}
</button>
</slot>
</div>
<div class="tabs-content">
<slot></slot>
</div>
</div>
`,
data() {
return {
tabs: []
}
},
mounted() {
this.tabs = this.$children
this.tabs[0].active = true
},
methods: {
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.active = tab === selectedTab
})
}
}
})
Vue.component('tab', {
props: {
name: String,
label: String
},
data() {
return {
active: false
}
},
template: `
<div v-show="active" class="tab-pane">
<slot></slot>
</div>
`
})
<tabs>
<tab name="home" label="首页">
<h2>首页内容</h2>
</tab>
<tab name="about" label="关于">
<h2>关于我们</h2>
</tab>
</tabs>
Vue.component('form-group', {
props: {
label: String,
error: String
},
template: `
<div class="form-group">
<label v-if="label">{{ label }}</label>
<slot></slot>
<span v-if="error" class="error">{{ error }}</span>
</div>
`
})
<form-group label="用户名" :error="errors.username">
<input v-model="form.username">
</form-group>
<form-group label="头像">
<file-upload @change="handleUpload"></file-upload>
</form-group>
Vue.component('dialog', {
props: {
visible: Boolean,
title: String
},
template: `
<div v-if="visible" class="dialog-overlay" @click.self="$emit('close')">
<div class="dialog">
<div class="dialog-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="close" @click="$emit('close')">×</button>
</div>
<div class="dialog-body">
<slot></slot>
</div>
<div class="dialog-footer">
<slot name="footer">
<button @click="$emit('close')">关闭</button>
</slot>
</div>
</div>
</div>
`
})
<dialog :visible="showDialog" @close="showDialog = false" title="确认">
<p>确定要删除吗?</p>
<template #footer>
<button @click="showDialog = false">取消</button>
<button @click="confirm">确定</button>
</template>
</dialog>
插槽内容的编译作用域
插槽内容在父组件作用域编译,无法访问子组件的数据:
<!-- ❌ 错误:无法访问子组件的 user -->
<user-card>
{{ user.name }}
</user-card>
<!-- ✅ 正确:使用作用域插槽 -->
<user-list :users="users" v-slot="{ user }">
{{ user.name }}
</user-list>
检查插槽是否存在
使用 $slots 检查插槽是否有内容:
Vue.component('card', {
template: `
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header"></slot>
</div>
<div class="card-body">
<slot></slot>
</div>
</div>
`
})
插槽要点:
<slot> 定义,传入内容替换name 属性区分多个插槽