【ElementPlus源码】Switch开关

时间:2024-10-19 18:36:46

文章目录

    • 基础用法 modelValue
    • 尺寸 size
    • 文字描述与自定义图标
    • 扩展的 value 类型
      • isControlled 是否受控
      • actualValue 实际值
      • checked 开关状态
    • 禁用状态、加载状态
    • 阻止切换 beforeChange
    • 切换 handleChange

看源码时候做的笔记。
Switch 开关 | Element Plus (element-plus.org)

基础用法 modelValue

在vue3中,v-modelmodelValue绑定的是同一个值。
v-model是一个语法糖,等同于同时使用:modelValue@update:modalValue,即向子组件传递一个名为modelValue的prop,同时监听一个名为update:modelValue的事件。

基础用法:绑定 v-model 到一个 Boolean 类型的变量。 会传到prop的modelValue中。

modelValue: {
    type: [Boolean, String, Number],
    default: false,
  },

尺寸 size

会传到prop的size中,是枚举值。

文字描述与自定义图标

  • active-text
  • inactive-text
  • inline-prompt

分别表示开关文案、控制文本是否显示在点内。源码中是驼峰式的,主要通过条件渲染实现。

  • inactive-icon
  • active-icon

开关icon。条件渲染实现。

扩展的 value 类型

  • activeValue
  • inactiveValue

绑定的开/关的对应值,类型可以为:Boolean, String, Number。

/**
   * @description switch value when in `on` state
   */
  activeValue: {
    type: [Boolean, String, Number],
    default: true,
  },
  /**
   * @description switch value when in `off` state
   */
  inactiveValue: {
    type: [Boolean, String, Number],
    default: false,
  },

switch开关底层是一个vue的input checkbox控件:

 <input
   :id="inputId"
   ref="input"
   :class="ns.e('input')"
   type="checkbox"
   role="switch"
   :aria-checked="checked"
   :aria-disabled="switchDisabled"
   :aria-label="label || ariaLabel"
   :name="name"
   :true-value="activeValue"
   :false-value="inactiveValue"
   :disabled="switchDisabled"
   :tabindex="tabindex"
   @change="handleChange"
   @keydown.enter="switchValue"
 />

aria表示无障碍。

true-valueactiveValuefalse-valueinactiveValue 替换 选中的true 和 未选中的false。

:true-value="activeValue"
:false-value="inactiveValue"

表单输入绑定 | Vue.js (vuejs.org)

因此传入的activeValueinactiveValue属性就为switch开/关的值。

isControlled 是否受控

代码中定义了一个变量isControlled表示是否受控。

const isControlled = ref(props.modelValue !== false)

定义类型时定义的modelValue默认为false,若此时modelValue不为false,说明switch组件是受控的,isControlled 为true,switch组件显示的值为传入值。

监听绑定的modelValue属性,由于监听的是属性,因此要传入一个getter。若modelValue发生变化,也说明此switch组件是受控的。

watch(
  () => props.modelValue,
  () => {
    isControlled.value = true
  }
)

actualValue 实际值

代码定义了一个变量actualValue表示实际值。

若switch是受控组件,则它的实际值为绑定的modelValue,否则为false。
若实际值actualValue与绑定为开的值activeValue相同,说明switch状态为开(checked=true)

const actualValue = computed(() => {
  return isControlled.value ? props.modelValue : false
})

const checked = computed(() => actualValue.value === props.activeValue)

到这里,actualValue的值可能是modelValue或false,可能出现不是activeValue / inactiveValue的情况(若不受控,则actualValue为false)。下面代码的目的是确保actualValue的值为activeValue / inactiveValue 。向父组件触发事件,入参为inactiveValue

if (![props.activeValue, props.inactiveValue].includes(actualValue.value)) {
  emit(UPDATE_MODEL_EVENT, props.inactiveValue)
  emit(CHANGE_EVENT, props.inactiveValue)
  emit(INPUT_EVENT, props.inactiveValue)
}

这三个事件分别对应:

export const UPDATE_MODEL_EVENT = 'update:modelValue'
export const CHANGE_EVENT = 'change'
export const INPUT_EVENT = 'input'

checked 开关状态

代码定义了一个变量checked表示开关状态。

const checked = computed(() => actualValue.value === props.activeValue)

参与控制前文所说的文字描述的显示与否。监听checked的变化,同步到input控件。

validateEvent即文档中的 validate-event属性,表示是否触发表单验证。如果需要触发的话,就触发。

watch(checked, (val) => {
  input.value!.checked = val

  if (props.validateEvent) {
    formItem?.validate?.('change').catch((err) => debugWarn(err))
  }
})

禁用状态、加载状态

定义了表单的禁用状态。若switchDisabled.value为true,则切换状态的方法switchValue会直接return。

const switchDisabled = useFormDisabled(computed(() => props.loading))

加载状态直接用条件渲染,不赘述。

阻止切换 beforeChange

设置beforeChange属性,若返回 false 或者返回 Promise 且被 reject,则停止切换。

若设置了beforeChange,则在触发切换switch前要先执行beforeChange。

在props中解构出beforeChange。若没有设置beforeChange,则直接切换。

beforeChange是一个返回Promise<boolean> | boolean>的方法。若它通过类型校验isPromiseOrBool ,则执行此方法。若promise.resolve的值能通过if判断,则执行切换:handleChange

const switchValue = () => {
  if (switchDisabled.value) return

  const { beforeChange } = props
  if (!beforeChange) {
    handleChange()
    return
  }

  const shouldChange = beforeChange()

  const isPromiseOrBool = [
    isPromise(shouldChange),
    isBoolean(shouldChange),
  ].includes(true)
  if (!isPromiseOrBool) {
    throwError(
      COMPONENT_NAME,
      'beforeChange must return type `Promise<boolean>` or `boolean`'
    )
  }

  if (isPromise(shouldChange)) {
    shouldChange
      .then((result) => {
        if (result) {
          handleChange()
        }
      })
      .catch((e) => {
        debugWarn(COMPONENT_NAME, `some error occurred: ${e}`)
      })
  } else if (shouldChange) {
    handleChange()
  }
}
  beforeChange: {
    type: definePropType<() => Promise<boolean> | boolean>(Function),
  },

文档的例子非常清晰:

<template>
  <el-switch
    v-model="value1"
    :loading="loading1"
    :before-change="beforeChange1"
  />
  <el-switch
    v-model="value2"
    class="ml-2"
    :loading="loading2"
    :before-change="beforeChange2"
  />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'

const value1 = ref(false)
const value2 = ref(false)
const loading1 = ref(false)
const loading2 = ref(false)

const beforeChange1 = () => {
  loading1.value = true
  return new Promise((resolve) => {
    setTimeout(() => {
      loading1.value = false
      ElMessage.success('Switch success')
      return resolve(true)
    }, 1000)
  })
}

const beforeChange2 = () => {
  loading2.value = true
  return new Promise((_, reject) => {
    setTimeout(() => {
      loading2.value = false
      ElMessage.error('Switch failed')
      return reject(new Error('Error'))
    }, 1000)
  })
}
</script>

切换 handleChange

val 是inactiveValue / activeValue 。在下一个 DOM 更新周期更新 input.value.checked 的值

const handleChange = () => {
  const val = checked.value ? props.inactiveValue : props.activeValue
  // 向外暴露一个input事件,参数为val
  emit(UPDATE_MODEL_EVENT, val)
  emit(CHANGE_EVENT, val)
  emit(INPUT_EVENT, val)
  //   在下一个 DOM 更新周期更新 input.value.checked 的值
  nextTick(() => {
    input.value!.checked = checked.value
  })
}

emit(CHANGE_EVENT, val)即文档中对外暴露的change事件。

其他内容,如switchKls这种使用ns 生成动态的类名等,在之前的系列里提到过,不再赘述。