1.Props | Vue.js (vuejs.org)

​ 父组件需要向子组件注入数据,可以使用props,类似于attrs:<comp label="xxxx">

特别注意!

  1. props是单向从父到子数据流,如果是子组件修改父组件属性,需要用#3 event

  2. 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>
  1. 父组件里面的<template #>等价于<template v-slot:default>,而且缺省的插槽可以不用写template,所以简化为<FancyButton/>

  2. 子组件的<slot name>是具名插槽,不加name就是default插槽!

  3. 可以有多个具名插槽,只能有一个default 插槽!

  4. 还可以将子组件的数据回传给父组件:

    <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>&nbsp;
		<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)

image-20240105154421455

如上图中,我们定义一个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事件~