实现一下经常用的 radio、radio-group 组件

151 阅读4分钟

一、经常用,里面是咋样的呢?

radioradio-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

radioemit也不需要很多,一个处理v-modelupdate,再给一个on-change就够用了

<script setup name="IRadio">
    // props ...
    
    const emit = defineEmits(['update:modelValue', 'on-change'])
</script>

3.slot

radioslot比较简单,就是用来接收radio的文字的,t提供一个default slot就好了

radio使用input去实现,而不是使用div去实现,这样做的好处在于可以保留浏览器默认的行为和快捷键,也就是说,浏览器知道这是一个选择框,至于丑的问题,用css解决即可

slotinput包裹在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-groupcontext - radioGroupContext,如果有的话

第四,在挂载的时候的判断radio是否在radio-group里面,useFindComponentUpward方法是根据传的componentName去找满足的父组件。如果找到了则说明外面包着的就是radio-group组件,那么就把当前radio组件的context通过radioGroupContextaddField保存起来,这样radio-group就能使用radio提供的方法和属性了

第五,change方法如果是insideGroup,则调用radioGroupContextchange方法处理

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

四、总结一下

比较好玩的是使用provideinject进行组件间的通信,另外示例代码缺少函数和组件的引用,如果要运行的话,需要自己补充一下