Vue3高阶组件技术
1.Props | Vue.js (vuejs.org)
父组件需要向子组件注入数据,可以使用props,类似于attrs:<comp label="xxxx">
特别注意!
props是单向从父到子数据流,如果是子组件修改父组件属性,需要用#3 event
props在子组件是只读的,意味我们无法修改props!
<script setup>
const props = defineProps(['foo']) console.log(props.foo)
const props2 = defineProps({
label: {
type: String,
require: true,
},
name: {
type: String,
require: true,
}
})
</script>
通常prop的玩法是: 1,通过父组件直接修改props绑定的变量,子组件内props自动更新界面!2、初始化给内部的原生组件,然后在原生组件事件里把prop值更新回去,所谓的v-model
模式。
PS,如果我们要强行额外写入属性怎么办呢,可以利用provide\inject
跨层级传输接口依赖注入 | Vue.js (vuejs.org)!:
provide('currentTab', currentTab) # 父组件写变量
inject("currentTab") # 子组件注入变量,注意,这个变量是Ref,并不位于props!
2.插槽 Slots | Vue.js (vuejs.org)
父组件需要向子组件注入代码使用slots。插槽就是父组件自定义代码,甚至可以是其他组件:
上图中红色的部分可以是任意html代码!子组件的蓝色slot区域只进行简单替换!
代码对应就是(父、子):
<FancyButton>
<template #>Click Me</template>
</FancyButton>
<template>
<button>
<slot name="default"></slot>
</button>
</template>
父组件里面的
<template #>
等价于<template v-slot:default>
,而且缺省的插槽可以不用写template
,所以简化为<FancyButton/>
子组件的
<slot name>
是具名插槽,不加name就是default
插槽!可以有多个具名插槽,只能有一个default 插槽!
还可以将子组件的数据回传给父组件:
<MyComponent v-slot="{ text, count }"> {{ text }} {{ count }} </MyComponent>
<script setup> const greetingMessage = 'hello' </script> <template> <div> <slot :text="greetingMessage" :count="1"></slot> </div> </template>
上述的用法,我们在el-table里面可以用来设置列特殊显示!
3. event组件事件 | Vue.js (vuejs.org)->双向数据绑定
/*
v-model在组件上正常工作需要:
1.将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop
2.当原生的 input 事件触发时,触发一个携带了新值的 update:modelValue 自定义事件
*/
<script setup>
import {ref,defineEmits,defineProps} from "vue"
const styleObj = ref({
margin: '5px',
font: '1em bold Consoles',
height: '26px'
})
defineEmits(['update:value'])
defineProps( ['title', 'options', 'value'])
</script>
<template>
<div :style="styleObj">
<label>{{ title }}</label>
<select :value="value" @change="$emit('update:value', $event.target.value)">
<option v-for="(value, key) in options" :value="key">{{ value }}</option>
</select>
</div>
</template>
上面@change调用emits
将值更新回value
,实现双向绑定!
<script setup>
import {ref} from "vue"
import MyComponent from './MyComponent.vue'
const options= { 'A': 'Move', 'B': 'Stop', 'C': 'Pause' }
const value = ref('A')
function submmit(){ console.log(value.value)}
</script>
<template>
<MyComponent title="Status" :options="options" v-model:value="value"/>
<input type="button" value="Submmit" @click="submmit" />
</template>
3 实现高阶组件:Tabs 标签页 | Element Plus (gitee.io)
如上图中,我们定义一个tabs
一级组件,内部又需要动态生成若干二级组件(外部slot注入二级组件内容!),使用方式为:
<template>
<tabs :active=0 @tab-click="handleClick">
<tab-pane label="User" name="first">
<!--Some Real Complex HTML CODE HER-->
<H1>
[1]-what's up
</H1>
</tab-pane>
<tab-pane label="Config" name="second">Config</tab-pane>
<tab-pane label="Role" name="third">Role</tab-pane>
<tab-pane label="Task" name="fourth">Task</tab-pane>
</tabs>
</template>
这里的[1]...
不能是父组件tabs
的defaultslot
,而是其子组件的tab-pane
插槽~,刚学会组件的童鞋可能会实现如下:
<!--Tabs.vue-->
<template>
<div>
<h2 class="title">{{ title }}</h2>
<div class="tab-wrapper" ref="childs">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const props =
defineProps<{
title: string,
active: Number,
}>();
const childs = ref(null); // 获取refs
onMounted(() => {
console.log(props.active);
const radio = childs.value.children[props.active].querySelector('.tab-radio')
console.log(radio);
radio.checked = true; // IDL 属性@https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes
// radio.setAttribute('checked', true);
})
</script>
<style scoped>
.tab-wrapper {
position: relative;
width: 100%;
overflow: visible;
height: 60px;
background-color: #33344a;
}
.title {
color: hsla(160, 100%, 37%, 1);
text-align: center;
font-weight: bold;
padding-bottom: 10px;
margin-bottom: 1px;
}
</style>
<!--TabPane.vue-->
<template>
<div>
<input type="radio" name="tab-radio" class="tab-radio" :id="id" :checked="active">
<label :for="id" class="tab-handler">{{ title }}</label>
<div class="tab-content">
<!--slot name="default" -->
<slot>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
defineProps<{
title: string,
active:boolean,
}>()
/**
* 生成一个用不重复的ID
*/
function GenNonDuplicateID() {
let idStr = Date.now().toString(36)
idStr += Math.random().toString(36).substr(3)
return idStr
}
const id = ref(GenNonDuplicateID());
</script>
<style scoped>
.tab-wrapper .tab-radio {
display: none;
}
.tab-handler {
position: relative;
z-index: 2;
display: block;
float: left;
height: 60px;
padding: 0 40px;
color: #717181;
cursor: pointer;
font-size: 18px;
line-height: 60px;
transition: .3s;
transform: scale(.9);
}
/* :not(p) */
.tab-radio:not(:checked)+.tab-handler:hover {
position: relative;
transition-property: all;
transition-duration: 0.2s;
transform: scale(1);
transition-timing-function: linear;
background-color: #ddd;
color: black;
}
/* 相邻兄弟选择器(+) */
.tab-radio:checked+.tab-handler {
color: whitesmoke;
background-color: hsla(160, 100%, 37%, 1);
transform: scale(1);
}
.tab-radio:checked+.tab-handler+.tab-content {
visibility: visible;
opacity: 1;
transform: scale(1);
}
.tab-wrapper .tab-content {
visibility: hidden;
position: absolute;
top: 60px;
left: 0;
width: 740px;
padding: 30px;
color: #999;
font-size: 14px;
line-height: 1.618em;
background-color: #fff;
opacity: 0;
transition: transform .5s, opacity .7s;
transform: translateY(20px);
}
</style>
这个代码虽然可用,仍然存在两个问题,第一是在使用了input[type=radio]
作为tab切换的触发器,CSS比较复杂,第二是 const radio = childs.value.children[props.active].querySelector('.tab-radio')
比较丑陋::jack_o_lantern:外部容器基本是啥事也不干!
下面开始正文,利用高阶组件技术完成slot嵌套,这里重点代码在父组件
!
-
第一步,父组件初始化,读取
default
插槽[defaultSlot],自动获取tab-pane
,并且将currentTab
响应式注入到子组件: -
第二步,处理标签页点击事件,直接将数据currentTab传给所有
slot #default
中的tab-pane组件 -
第三步,tab-pane组件自行显示或者隐藏,实现自动切换!
<!--tabs.vue-->
<template>
<div div="tab-contain">
<h2 class="tab-title">{{ title }}</h2>
<div class="tab-bar">
<!-- 渲染tab标签 -->
<div v-for="(tab, index) in titles" :key="index" :class="{ 'active': activeTab === index }"
@click="selectTab(index)">
{{ tab.label }}
</div>
</div>
<!-- 渲染选中的内容 -->
<div class="tab-panes">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { ref, useSlots, provide } from 'vue';
const props = defineProps({
title: {
type: String,
default: ""
},
active: {
type: Number, // 默认激活的tab索引
default: 0,
}
})
const activeTab = ref(props.active)
// const tabs = ref([]) // tab数据
const titles = ref([])
const currentTab = ref("")
const slots = useSlots();
const defaultSlot = slots.default();
let index = 0;
const tabPanelName = "TabPane"
// 遍历子组件,修改props
defaultSlot.forEach((vnode) => {
if (vnode.type.name === tabPanelName || vnode.type.__name === tabPanelName) {
console.log(vnode)
const { label, name } = vnode.props;
titles.value.push({ label, name })
if (activeTab.value == index) {
currentTab.value = name;
}// 修改props
// tabs.value.push(vnode)
++index;
}
});
provide('currentTab', currentTab) // 在父组件中通过 provide 声明要传递的数据,并将其设置为响应式的
function selectTab(index) {
this.activeTab = index; // 切换激活的tab
this.currentTab = this.titles[this.activeTab].name;
console.log(index, currentTab)
}
</script>
<style scoped>
.tab-contain {}
.tab-title {
color: hsla(160, 100%, 37%, 1);
text-align: center;
font-weight: bold;
padding-bottom: 10px;
margin-bottom: 1px;
}
.tab-bar {
display: flex;
height: 60px;
width: 100%;
color: white;
background-color: #33344a;
}
.tab-bar>div {
flex-basis: 150px;
height: 60px;
line-height: 60px;
cursor: pointer;
text-align: center;
}
.tab-bar div.active {
background-color: hsla(160, 100%, 37%, 1);
}
.tab-panes {
position: relative;
width: 100%;
padding: auto;
color: #333;
font-size: 14px;
line-height: 1.618em;
background-color: #fff;
}
</style>
<!--tabPane.vue-->
<template>
<!-- 这里使用类绑定实现隐藏/显示tabpane,也可以直接使用v-if+transition!-->
<div :class="{ 'active': currentTab === name }" class="panel">
<slot></slot>
</div>
</template>
<script setup>
import { inject } from 'vue';
const props = defineProps({
label: {
type: String,
require: true,
},
name: {
type: String,
require: true,
}
})
let currentTab = inject('currentTab') // 在子组件中使用 inject 来接收这些数据。
</script>
<style lang="css" scoped>
.panel {
padding:20px 30px;
visibility: hidden;
position: absolute;
color: #333;
font-size: 14px;
line-height: 1.618em;
background-color: #fff;
transition: transform .5s, opacity .7s;
transform: translateY(20px);
}
.active {
visibility: visible;
opacity: 1;
transform: scale(1);
}
</style>
外部使用方式和·el-tab/el-tab-Pane
基本一致,当然还能添加一个tabClick
事件~