advanced-vue-component-design 之受控组件
总结实用(项目里能马上用到的)的 vue 组件设计方法,让封装组件更加容易,代码更加好维护。
受控组件 controlled component
受控组件:组件的状态完全由 vue 控制的组件。vue 和 react 中的受控组件和非受控组件的概念是一样的。
常见的表 input、select 等表单元素带有 value 属性,具有自己的状态,如果这些状态由 vue 控制,就就变受控组件。
vue 通过v-model
来实现受控组件,自定义组件上使用 v-model 非常容易。
比如MyInput
组件,vue3 中可这样使用。
<MyInput v-model="myInput" v-model:title="title" />
MyInput 的实现:
<template>
<div>
<p>title</p>
<input type="text" name="title" :value="title" @input="onInputTitle" />
<p>content</p>
<input type="text" name="my-input" :value="modelValue" @input="onInput" />
</div>
</template>
<script>
import { ref, reactive, watch, computed, defineComponent } from 'vue'
export default defineComponent({
name: 'MyInput',
props: ['modelValue', 'title'],
setup(props, { emit, attrs, slots }) {
// console.log(props.title)
function onInput(event) {
emit('update:modelValue', event.target.value)
}
function onInputTitle(event) {
emit('update:title', event.target.value)
}
return { onInputTitle, onInput }
},
})
</script>
v-model 指令的原理:modelValue 绑定到 value 上和在 input 事件中触发update:modelValue
自定义事件。
如何修改 prop 的名字和自定义事件呢?
v-model:propName + update:propName
默认情况下,v-model 不需要指定参数,修改 prop 的名字后,需要指定参数,比如上面的v-mode:title
。vue3 加强了 v-model,可实现传递多个值,比如上面的 <MyInput v-model="myInput" v-model:title="title" />
。
如何使用受控组件实现一个类似微信的 switch 组件?
先看使用方式:
<ToggleInput v-model="toggled" />
直接上代码:
<template>
<span
class="toggle"
tabindex="0"
role="checkbox"
:aria-checked="modelValue"
@click="toggle"
@keydown.space.prevent="toggle"
></span>
</template>
<script>
import { ref, reactive, watch, computed, defineComponent } from 'vue'
export default defineComponent({
name: 'ToggleInput',
props: ['modelValue'],
setup(props, { emit, attrs, slots }) {
function toggle() {
console.log('点击', props.modelValue)
emit('update:modelValue', !props.modelValue)
}
return { toggle }
},
})
</script>
<style scoped lang="scss">
$border-radius: 9999px;
$toggle-width: 3rem;
$toggle-height: 1.5rem;
.toggle {
/* background-color: red; */
position: relative;
display: inline-block;
height: $toggle-height;
width: $toggle-width;
flex-shrink: 0;
border-radius: $border-radius;
cursor: pointer;
/* 聚焦时样式 */
&:focus {
outline: 0;
box-shadow: 0 0 0 2px rgba(52, 144, 220, 0.5);
}
&:before {
display: inline-block;
border-radius: $border-radius;
height: 100%;
width: 100%;
content: '';
background-color: #dae1e7;
/* background-color: red; */
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
transition: background-color 0.2s ease;
}
/* 开启后背景颜色变为绿色 */
&[aria-checked='true']:before {
background-color: #04be02;
}
/* 移动的按钮,关闭时位于左侧,left:0 */
&:after {
position: absolute;
top: 0;
left: 0;
height: $toggle-height;
width: $toggle-height;
background-color: #fff;
border-radius: $border-radius;
border-width: 1px;
border-color: #dae1e7;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 10%);
content: '';
transform: translateX(0);
transition: transform 0.2s ease;
}
/* 开启后,向右移动 0.5 的 span 宽度 */
&[aria-checked='true']:after {
transform: translateX(#{$toggle-height});
}
}
</style>
tabindex="0" 的作用是什么?
使得元素是可聚焦的,并且可以通过键盘导航来聚焦到该元素。
role="checkbox" 的作用是什么?
告诉屏幕阅读器,这是多选框。
aria-checked 的作用是什么?
告诉屏幕阅读器,元素选中状态。ARIA (Accessible Rich Internet Applications) 是一组属性,用于定义使残障人士更容易访问 Web 内容和 Web 应用程序。
role、aria-* 都是为了实现无障碍访问添加的属性
实现的关键点:
使用绝对定位放置圆形按钮,且使用伪元素。
当状态改变时,移动元素按钮,同时改变组件的背景色。
再思考 v-model 的使用
在原生标签上
<input v-model="inputValue" />
<!-- 等价于 -->
<input :value="inputValue" @input="inputValue=$event.target.value" />
自定义在组件上
<custom-input :model-value="searchText" @update:model-value="searchText = $event"></custom-input>
组件实现v-model
:声明model-value
的 prop, 将该 prop 绑定到 value 上,在 value 改变时,通过自定义事件 update:modelValue
把最新的值抛出。
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`,
})
v-model 在自定义组件上使用:
<custom-input v-model="searchText"></custom-input>
v-model 如何是计算属性结合使用?
在 set 中触发自定义事件。
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input v-model="value">
`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
},
},
},
})
这种实现在二次封装表单组件时,特别有用。
什么时候使用
modelValue
+update:modelValue
代替 v-model?
组件在循环中时,比如下面的例子。
<div v-for="item in metricList" :key="item.id">
<!-- NOTE 使用 v-model 报错 -->
<MetricOperate :model-value="item" @update:model-value="onUpdateModelValue" />
</div>