一、经常用,里面是咋样的呢?
radio
和radio-group
都用不少吧,在表单里面常用来做多个选项的单选操作。那他里面是咋封装的呢,今天来瞧瞧
radio
可以单独使用,但是更常见的是搭配radio-group
一起使用,达到多选项单选的效果
<template>
<div>
<i-radio v-model="radioVal">
选项1
</i-radio>
{{ radioVal }}
<i-radio-group v-model="radioGroupVal">
<i-radio value="1">选项 1</i-radio>
<i-radio value="2">选项 2</i-radio>
<i-radio value="3">选项 3</i-radio>
<i-radio value="4">选项 4</i-radio>
</i-radio-group>
{{ radioGroupVal }}
</div>
</template>
可以看出,radio
组件单独使用和搭配radio-group
使用的传参是不一样的,为了避免混淆,我们先实现radio
二、radio的实现
一个组件的封装从3个API入手,分别是prop
, emit
, slot
1. prop
radio
传参很简单,就一个value
,但是要考虑到禁用,所以再加一个disabled
<script setup name="IRadio">
const props = defineProps({
modelValue: {
type: [Number, String, Boolean],
default: false
},
disabled: {
type: Boolean,
default: false
}
})
</script>
2.emit
radio
的emit
也不需要很多,一个处理v-model
的update
,再给一个on-change
就够用了
<script setup name="IRadio">
// props ...
const emit = defineEmits(['update:modelValue', 'on-change'])
</script>
3.slot
radio
的slot
比较简单,就是用来接收radio
的文字的,t提供一个default slot
就好了
radio
使用input
去实现,而不是使用div
去实现,这样做的好处在于可以保留浏览器默认的行为和快捷键,也就是说,浏览器知道这是一个选择框,至于丑的问题,用css解决即可
slot
和input
包裹在label
中,这样在点击slot
文字的时候,input
也会被触发
<template>
<label>
<span>
<input
type="radio"
:checked="currentValue"
:disabled="disabled"
:class="disabled ? 'is-disabled' : ''"
@change="change"
>
</span>
<slot></slot>
</label>
</template>
4.加一点逻辑
把模板里面缺少的参数加上
<template>
...
</template>
<script setup name="IRadio">
// props ...
// emit
const currentValue = ref<any>(props.modelValue)
watch(() => props.modelValue, (val) => {
currentValue.value = val
})
const change = (event) => {
if (props.disabled) {
return false
}
const checked = event.target.checked
currentValue.value = checked
emit('update:modelValue', checked)
emit('on-change', checked)
}
</script>
5.使用
<template>
<div>
<i-radio v-model="radioVal">
选项1
</i-radio>
{{ radioVal }}
</div>
</template>
<script lang="ts" setup>
const radioVal = ref(false)
</script>
初始值
点击后
三、radio-group 的实现
从
radio
的单独使用来看,没有实际的使用场景,所以还是需要搭配radio-group
使用
我们还是先从prop
, emit
, slot
入手
1.prop、emit、slot
radio-group
的这3个API都很简单,可以参照radio
去快速实现
<template>
<div>
<slot></slot>
</div>
</template>
<script lang="ts" setup name="IRadioGroup">
const props = defineProps({
modelValue: {
type: [Number, String, Boolean],
default: false
},
disabled: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'on-change'])
</script>
2.补充 radio-group 的逻辑
<script lang="ts" setup name="IRadioGroup">
// prop...
// emit...
watch(() => props.modelValue, () => {
updateRadioChecked()
})
watch(() => props.disabled, () => {
initRadioDisabled()
})
// 保存 radio 的 context
const children = ref<any>([])
const addField = (child) => {
children.value.push(child)
}
const initRadiosName = () => {
// 生成一个5位的id,可以使用随机数代替
const id = uuid(5)
if (children.value.length) {
children.value.forEach(child => {
// 设置radio的name
child.setRadioName(id)
})
}
}
const updateRadioChecked = () => {
if (children.value.length) {
children.value.forEach(child => {
// 设置radio的checked
child.updateChecked(props.modelValue)
})
}
}
const initRadioDisabled = () => {
if (children.value.length) {
children.value.forEach(child => {
// 设置radio的disabled
child.updateDisabled(props.disabled)
})
}
}
onMounted(() => {
initRadiosName()
updateRadioChecked()
if (props.disabled) {
initRadioDisabled()
}
})
const change = (value) => {
emit('update:modelValue', value)
emit('on-change', value)
formItemContext?.validate('change')
}
const context = reactive({
addField,
change
})
provide('i-radio-group-context', context)
</script>
主要解释一下provide('i-radio-group-context', context)
,这代表向radio
提供一个context
,这样子radio
就能够使用radio-group
提供的一些方法和属性了
3.改造 radio
先看代码
<template>
<label>
<span>
<input
v-if="insideGroup"
type="radio"
:name="radioName"
:value="value"
:checked="currentValue"
:disabled="radioDisabled"
:class="radioDisabled ? 'is-disabled' : ''"
@change="change"
>
<input
v-else
type="radio"
:checked="currentValue"
:disabled="disabled"
:class="disabled ? 'is-disabled' : ''"
@change="change"
>
</span>
<slot></slot>
</label>
</template>
<script lang="ts" setup name="IRadio">
const props = defineProps({
// ...
value: {
type: [Number, String, Boolean],
default: false
}
})
const change = () => {
// ...
emit('update:modelValue', checked)
if (insideGroup.value) {
radioGroupContext.change(event.target.value)
} else {
emit('on-change', checked)
formItemContext?.validate('change')
}
}
const radioName = ref('')
const setRadioName = id => {
radioName.value = id
}
const updateChecked = val => {
currentValue.value = val === props.value
}
const groupDisabled = ref(false)
const updateDisabled = val => {
groupDisabled.value = val
}
const radioDisabled = computed(() => {
return props.disabled || groupDisabled.value
})
const context = reactive({
setRadioName,
updateChecked,
updateDisabled
})
const insideGroup = ref(false)
const radioGroupContext: any = inject('i-radio-group-context', undefined)
const radioParent = useFindComponentUpward('IRadioGroup')
onMounted(() => {
if (radioParent) {
insideGroup.value = true
}
if (insideGroup.value) {
radioGroupContext?.addField(context)
}
})
</script>
是不是加了很多东西,我们一一看
第一,prop
补充了value
,代表当前radio
的值
第二,模板使用insideGroup
去区分不同的input
,特别注意加上了name
参数,用于让radio
成组
第三,获取radio-group
的context
- radioGroupContext
,如果有的话
第四,在挂载的时候的判断radio
是否在radio-group
里面,useFindComponentUpward
方法是根据传的componentName
去找满足的父组件。如果找到了则说明外面包着的就是radio-group
组件,那么就把当前radio
组件的context
通过radioGroupContext
的addField
保存起来,这样radio-group
就能使用radio
提供的方法和属性了
第五,change
方法如果是insideGroup
,则调用radioGroupContext
的change
方法处理
4.使用试试
<template>
<div>
<i-radio-group v-model="radioVal">
<i-radio value="1">选项 1</i-radio>
<i-radio value="2">选项 2</i-radio>
<i-radio value="3">选项 3</i-radio>
<i-radio value="4">选项 4</i-radio>
</i-radio-group>
{{ radioVal }}
</div>
</template>
<script>
const radioVal = ref('4')
</script>
默认选中选项4
手动选择选项3
四、总结一下
比较好玩的是使用provide
和inject
进行组件间的通信,另外示例代码缺少函数和组件的引用,如果要运行的话,需要自己补充一下