一、经常用,里面是咋样的呢?
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进行组件间的通信,另外示例代码缺少函数和组件的引用,如果要运行的话,需要自己补充一下