Element-ui 之 Radio 组件的源码分析

此篇文章将从Radio组件提供的属性和事件的角度,逐步分析element-uiRadio组件源代码,从而介绍如何实现一个完整的Radio组件。

一. Radio 组件提供的属性和事件

1.1 属性

以下是 Radio 组件提供的属性:

参数说明类型可选值默认值
value / v-model绑定值string / number / boolean
labelRadio 的 valuestring / number / boolean
disabled是否禁用booleanfalse
border是否显示边框booleanfalse
sizeRadio 的尺寸,仅在 border 为真时有效stringmedium / small / mini
name原生 name 属性string

1.2 事件

事件名称说明回调参数
input绑定值变化时触发的事件选中的 Radio label 值

二. 按照步骤去实现Radio 组件的属性和事件

  1. 注册组件。
  2. 完成组件基本的 template 部分。
  3. 设置组件的绑定值 value/v-modelRadiovalue 以及Radio的中文描述。
  4. 设置禁用属性 disabled
  5. 设置是否显示边框属性 border
  6. 设置 Radio 组件尺寸的 size 属性。
  7. 设置原生 name 属性。
  8. 支持屏幕阅读器的识别。
  9. 增加元素 focus 的样式。

三. Radio 组件属性和事件的具体实现

3.1 注册组件

新建一个 radio.vue 文件,设置组件的 namecomponentName 都为 ElRadio
main.js 文件里面引入这个组件,并且使用 Vue.component 来全局注册这个组件。

radio.vue 文件:

<template>
  <label
    ...
  </label>
</template>
<script>
export default {
   name: 'ElRadio',
   componentName: 'ElRadio',
}

</script>

main.js 注册组件:

import Radio from './components/radio/radio';

const components = [
  Radio,
]

components.forEach(component => {
  Vue.component(component.name, component);

});

3.2 完成组件基本的 template 部分

<template>
  <!-- 整体用 label 标签包裹 -->
  <label
   class="el-radio"
   role="radio"
  >
   <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
   <span class="el-radio__input">
     <span class="el-radio__inner"></span>
     <input
       class="el-radio__original"
       type="radio"
       autocomplete="off"
     />
   </span>
   <!-- radio 右侧的描述 -->
   <span class="el-radio__label">
     ...
   </span>
  </label>
</template>
  • Radio 组件整体用 label 标签包裹。
  • Radio 组件左侧的点击按钮是用 span 标签做样式,将 input type=radio 放置在 span 标签的下方。
  • Radio 组件右侧的描述用 span 标签包裹。

3.3 设置组件的绑定值 value/v-model 、 Radio 的 value 以及Radio的中文描述

3.3.1 设置 Radio 的 value

描述:
将父组件传入的 label 属性作为 Radio 组件中 input type="radio" 标签的 value 属性值。

实现步骤:

父组件:将 label 的值传过去。

<template>
  <div>
   <el-radio label="1">选项1</el-radio>
   <el-radio label="2">选项2</el-radio>
  </div>
</template>

Radio 组件:

  1. 组件内部接收父组件传过来的 label 属性。
  2. type=radio 的单选按钮的 value 设置为 label 的值。

Radio.vue 文件:

<template>
  <!-- 整体用 label 标签包裹 -->
  <label
   class="el-radio"
   role="radio"
  >
   <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
   <span class="el-radio__input">
     <span class="el-radio__inner"></span>
     <!-- 将 type=radio 的单选按钮的 value 设置为 label 的值 -->
     <input
       class="el-radio__original"
       type="radio"
       :value="label"
       autocomplete="off"
     />
   </span>
   <!-- radio 右侧的描述 -->
   <span class="el-radio__label">
    ...
   </span>
  </label>
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 接收父组件传过来的 label 属性
    label: {}
  }
}
</script>

3.3.2 设置 Radio 的中文描述

实现步骤:

  1. slot 插槽接收父组件传过来的标签内的内容。
  2. 如果父组件标签内没有内容,则展示 label 属性内容。

Radio.vue 文件:

<!-- radio 右侧的描述 -->
<span class="el-radio__label">
  <slot></slot>
  <template v-if="!$slots.default">{{ label }}</template>
</span>
  • $slots 用来访问被插槽分发的内容,$slots.default 包含了所有没有被包含在具名插槽下的节点。

3.3.3 实现绑定值 value/v-model

实现步骤:

父组件:使用 v-model 传入值。

<template>
  <div>
    <el-radio v-model="radio" label="1">选项1</el-radio>
    <el-radio v-model="radio" label="2">选项2</el-radio>
  </div>
</template>
<script>
export default {
  data() {
    return {
      radio: "1"
    }
  }
}
</script>
  • 在自定义组件使用 v-model="radio" 相当于对其绑定了 value 值和 input 方法 :value="radio" @input="radio = arguments[0]"

Radio 组件:

  1. Radio 组件内部默认使用 value 去接收值,并且设置计算属性 model 监听 value 值的改变,并且为其设置 gettersetter。在 getter 中获取 value 的值,在 setter 中使用 $emitinput 事件发送给父组件,并且将 value 值传递给父组件。
<template>
  ...
  <span class="el-radio__input">
    <span class="el-radio__inner"></span>
    <!-- 将 input 的 v-model 设置为 model 变量 -->
    <input
      class="el-radio__original"
      type="radio"
      v-model="model"
      :value="label"
      autocomplete="off"
    />
  </span>
  ...
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 自定义组件的 v-model 会默认把 value 用作 prop
    value: {},
  },
  computed: {
    model: {
      get() {
        // 获取 value 的值
        return this.value;
      },
      set(val) {
        // 在 setter 中使用 $emit 将 input 事件发送给父组件,并且将 value 值传递给父组件
        this.$emit('input', val)
      }
    }
  }
}
</script>
  1. 改变选中时给 input 增加 checked 属性并增加选中样式,并且让父组件监听到 change 事件。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 当 model 与 label 相等时增加 is-checked 的 class 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-checked': model === label },
    ]"
    role="radio"
  >
    ...
    <!-- 给 Input 增加 change 事件 -->
    <input
      ref="radio"
      class="el-radio__original"
      type="radio"
      v-model="model"
      :value="label"
      @change="handleChange"
      autocomplete="off"
    />
    ...
  </label>
</template>
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 自定义组件的 v-model 会默认把 value 用作 prop
    value: {},
  },
  computed: {
    model: {
      get() {
        // 获取 value 的值
        return this.value;
      },
      set(val) {
        // 在 setter 中使用 $emit 将 input 事件发送给父组件,并且将 value 值传递给父组件
        this.$emit('input', val)
        // 当 model 与 label 相等时,给 radio 增加 checked 属性
        this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
      }
    }
  },
  methods: {
    handleChange() {
      this.$nextTick(() => {
        // 当 Input 触发 change 事件时,触发父组件的change事件,并且将最新的值传递给父组件
        this.$emit('change', this.model);
      })
    }
  }
}
</script>

3.3.4 设置禁用属性 disabled

实现步骤:

  1. 监听父组件传过来的 disabled 属性,并且设置计算属性 isDisabled 监听 disabled 的改变。
<script>
export default {
  name: 'ElRadio',
  componentName: 'ElRadio',
  props: {
    // 监听父组件传递的 disabled 属性
    disabled: Boolean,
},
  computed: {
    ...
    // 设置计算属性 isDisabled 监听 disabled 的改变
    isDisabled() {
      return this.disabled;
    }
  },
}
</script>
  1. Input 标签增加 disabled 属性,并且在最外层的 labelRadio 左侧的点击按钮分别增加 disabled 的样式。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 给最外层的 label 增加 disabled 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-disabled': isDisabled },
      ...
    ]"
  >
    <!-- 给点击按钮增加 disabled 样式 -->
    <span class="el-radio__input"
      :class="[
        { 'is-disabled': isDisabled },
        ...
      ]"
    >
      <span class="el-radio__inner"></span>
      <!-- 给 Input 增加 disabled 属性 -->
      <input
        ref="radio"
        class="el-radio__original"
        type="radio"
        v-model="model"
        @change="handleChange"
        :value="label"
        :disabled="isDisabled"
      />
    </span>
    ...
  </label>
</template>

3.3.5 设置是否显示边框属性 border

实现步骤:

  1. 监听父组件传过来的 border 属性。
props: {
  disabled: Boolean,
}
  1. 在最外层的 label 上面增加 border 样式。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 给最外层的 label 增加 border 样式 -->
  <label
    class="el-radio"
    :class="[
      { 'is-bordered': border },
      ...
    ]"
  >
    ...
  </label>
</template>

3.3.6 设置Radio组件尺寸的 size 属性

实现步骤:

  1. 监听从父组件传过来的 size 属性,并设置 radioSize 计算属性监听 size 的改变。
props: {
  disabled: Boolean,
}
...
computed: {
  radioSize() {
    return this.size
  },
}
  1. 判断在设置 size 属性之前,是否设置了 border 属性,如果设置了将 size 的样式添加到最外层的 label 上面。
<template>
  <!-- 整体用 label 标签包裹 -->
  <!-- 判断在设置 size 属性之前,是否设置了 border 属性,如果设置了将 size 的样式添加到最外层的 label 上面 -->
  <label
    class="el-radio"
    :class="[
      border && radioSize ? 'el-radio--' + radioSize : '',
      ...
    ]"
  >
    ...
  </label>
</template>

3.3.7 设置原生name属性

name 属性可设置或返回单选按钮的名称,name 属性用于表单提交后向服务器传送数据。

实现步骤:

  1. 监听父组件传过来的 name 属性。
props: {
  name: String,
}
  1. name 属性绑定在 input 上。
<!-- 将 name 属性绑定在 input 上 -->
<input
  ref="radio"
  class="el-radio__original"
  type="radio"
  v-model="model"
  @change="handleChange"
  :name="name"
  :value="label"
  :disabled="isDisabled"
  autocomplete="off"
/>

3.3.8 支持屏幕阅读器识别

屏幕阅读器相关属性介绍:

  1. role 属性:告诉辅助设备(如屏幕阅读器)这个元素扮演的角色,为了增强语义性。
  2. aria 开头的属性:可以为元素提供一些信息,以便屏幕阅读器可以正确的解读,使残障人士更容易访问Web内容或Web应用程序。
  3. tabindex 属性:为了网页的无障碍访问,表示根据 tab 键控制的元素焦点的次序。
    • tabindex 为负整数,通常设置为-1,此时元素不能通过 tab 键聚焦,可以通过 js 聚焦。
    • tabindex 设置为0,元素可以聚焦,聚焦的顺序是按照元素所处的 DOM 结构决定的。

实现步骤:

  1. 增加 aria- 开头的属性。

(1)在最外层的 role=radiolabel 元素上面增加 aria-checkedaria-disabled

// 当 model 与 label 相等时,即为选中
:aria-checked="model === label"
// 当变量 isDisabled 为 true 时,即为禁用
:aria-disabled="isDisabled"

(2)在 Input 元素上面增加 aria-hidden

// 在屏幕阅读器上面隐藏该元素
aria-hidden="true"
  1. 增加使用 tab 键控制元素焦点,使用空格键选中的功能。

(1)设置计算属性 tabIndex,监听如果 disabled 不为 true 则可以聚焦,否则不可以聚焦,并且将 tabindex 设置给外层的 label

<label
  :tabindex="tabIndex"
>
</label>
...
computed: {
  tabIndex() {
    return this.isDisabled ? -1 : 0;
  }
}

(2)外层 label 在点击空格时,判断该选项是否是禁用的,如果是禁用的,则 model 不变,否则 model 等于当前 label

<label
  @keydown.space.stop.prevent="model = isDisabled ? model : label"
>
</label>

(3)内层 input 元素的 tabindex 设置为 -1,禁止聚焦。

<input
  tabindex="-1"
</input>

(4)右侧描述禁止键盘事件冒泡。当 Radio 组件传入的 slot 为输入框时,避免点空格的时候,冒泡到上层元素,变为切换选中。

<!-- 禁止键盘事件冒泡 -->
<span class="el-radio__label" @keydown.stop>
  <slot></slot>
  <template v-if="!$slots.default">{{ label }}</template>
</span>

3.3.9 增加元素 focus 的 class

实现步骤:

  1. 增加 focus 变量,及 focus 样式的 class
// 最外层 label 绑定 class
{ 'is-focus': focus },

// input 增加 focus 和 blur 事件
@focus="focus = true"
@blur="focus = false"

data() {
  return {
    focus: false
  };
},
  1. 有关 focus 样式的源代码处理。
&:focus:not(.is-focus):not(:active):not(.is-disabled){ /*获得焦点时 样式提醒*/
  box-shadow: 0 0 2px 2px $--radio-button-checked-border-color;
}

四. 整体代码详解

包含 RadioRadio-Group 组件以及 Form 组件相互作用的部分。

<template>
  <!-- 整体用 label 标签包裹 -->
  <label
    class="el-radio"
    :class="[
      border && radioSize ? 'el-radio--' + radioSize : '',
      { 'is-disabled': isDisabled },
      { 'is-focus': focus },
      { 'is-bordered': border },
      { 'is-checked': model === label },
    ]"
    role="radio"
    :aria-checked="model === label"
    :aria-disabled="isDisabled"
    :tabindex="tabIndex"
    @keydown.space.stop.prevent="model = isDisabled ? model : label"
  >
    <!-- radio 左侧的点击按钮,用 span 做样式,将 input type=radio 放置在 span 标签的下方 -->
    <span class="el-radio__input"
      :class="[
        { 'is-disabled': isDisabled },
        { 'is-checked': model === label }
      ]"
    >
      <span class="el-radio__inner"></span>
      <input
        ref="radio"
        class="el-radio__original"
        type="radio"
        aria-hidden="true"
        v-model="model"
        @focus="focus = true"
        @blur="focus = false"
        @change="handleChange"
        :name="name"
        :value="label"
        :disabled="isDisabled"
        tabindex="-1"
        autocomplete="off"
      />
    </span>
    <!-- radio 右侧的描述 -->
    <span class="el-radio__label">
      <slot></slot>
      <template v-if="!$slots.default">{{ label }}</template>
    </span>
  </label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';

export default {
  name: 'ElRadio',
  
  mixins: [Emitter],
  
  inject: {
    elForm: {
      default: ''
    },
    elFormItem: {
      default: ''
    }
  },
  
  componentName: 'ElRadio',
  
  props: {
    value: {},
    label: {},
    disabled: Boolean,
    name: String,
    border: Boolean,
    size: String
  },
  
  data() {
    return {
      focus: false
    };
  },
  
  computed: {
    // 判断是不是按钮组
    isGroup() {
      // parent 变量保存组件的父级元素信息
      let parent = this.$parent;
      while (parent) {
        if (parent.$options.componentName !== 'ElRadioGroup') {
          // 如果父级的 componentName 不是 ElRadioGroup 则继续向上查找
          parent = parent.$parent;
        } else {
          // 如果父级的 componentName 是 ElRadioGroup
          // 用变量 _radioGroup 保存父级信息,并且返回 true
          this._radioGroup = parent;
          return true;
        }
      }
      return false;
    },
    // input 绑定的 v-model
    model: {
      get() {
        // 判断是不是按钮组,如果是按钮组,取按钮组的 value 值,否则取当前组件的 value 值
        return this.isGroup ? this._radioGroup.value : this.value;
      },
      set(val) {
        // 当值改变时,判断是不是按钮组,如果是按钮组,则触发按钮组组件的 input 事件,否则触发当前组件的 input 事件
        if (this.isGroup) {
          this.dispatch('ElRadioGroup', 'input', [val]);
        } else {
          this.$emit('input', val);
        }
        // 设置当前 radio 的 checked 属性,如果 model 与 label 相等,则为选中
        this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
      }
    },
    // 获取 Form-Item 组件的 elFormItemSize 变量
    _elFormItemSize() {
      return (this.elFormItem || {}).elFormItemSize;
    },
    // Radio 的尺寸计算
    radioSize() {
      // 设置变量 temRadioSize,优先获取组件内部的 size,再获取 Form-Item 组件的 elFormItemSize
      const temRadioSize = this.size || this._elFormItemSize;
      // 按钮组优先级:group-size > 组件内部size > Form-Item > Form
      // 按钮优先级:组件内部size > Form-Item > Form
      return this.isGroup
          ? this._radioGroup.radioGroupSize || temRadioSize
          : temRadioSize;
    },
    // disabled 计算
    isDisabled() {
      // 按钮组优先级:group > 组件内部 disabled > Form
      // 按钮优先级:组件内部 disabled > Form
      return this.isGroup
        ? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
        : this.disabled || (this.elForm || {}).disabled;;
    },
    // 判断是否用 tab 键可以聚焦
    tabIndex() {
      // 如果是禁用的,或者是按钮组且当前选中的按钮不是这个,则不能聚焦,否则可以聚焦
      return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
    }
  },
  
  methods: {
    handleChange() {
      this.$nextTick(() => {
        // 给组件增加 change 事件
        this.$emit('change', this.model);
        this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
      })
    }
  }
}
</script>
<think>好的,我现在需要解决用户在Element UI的el-radio-group组件中实现文字换行的问题。首先,我应该回忆一下Element UI的文档和相关组件的结构。根据用户提供的引用内容,特别是引用[1]和[2],可以看到el-radio-group通常包裹多个el-radio或el-radio-button组件。用户提到文字换行,可能是指radio选项的标签文字过长时无法自动换行,导致显示不全或布局错乱。 接下来,我需要分析导致文字不换行的原因。通常,这可能是由于CSS样式的限制,比如white-space属性被设置为nowrap,或者容器宽度不足,导致文本无法换行。查看Element UI的默认样式,可能el-radio或el-radio-button的label部分设置了不换行属性。 根据引用[3]和[4],虽然源码部分提到了emitter.js中的dispatch方法,但文字换行主要涉及样式问题,因此可能需要覆盖默认的CSS。用户可能需要自定义样式,强制文本换行。例如,可以检查.el-radio__label类的white-space属性,并修改为normal。 另外,引用[1]中提到使用div包裹el-radio-button来实现垂直排列,这说明结构上可以通过嵌套div来调整布局。同样,文字换行可能需要调整内部元素的显示方式,比如设置display: inline-block或调整宽度。 可能的解决方案包括: 1. 使用自定义样式覆盖Element UI的默认样式,设置white-space: normal和word-break: break-all。 2. 调整el-radio或el-radio-button的宽度,允许内容区域扩展。 3. 使用插槽(slot)来自定义label内容,添加换行标签或样式。 需要验证这些方法的可行性。例如,在el-radio-button中,标签文本可能在span元素内,通过CSS修改该元素的white-space属性。同时,考虑到不同版本的Element UI可能存在差异,需要确保样式正确应用,可能需要使用/deep/或::v-deep来穿透scoped样式。 此外,用户可能遇到在el-radio-group内多个选项的文字换行问题,可能需要为每个选项单独设置样式,或者全局覆盖。例如,在组件的样式中添加: .el-radio__label { white-space: normal !important; word-break: break-all; } 如果用户使用的是el-radio-button,对应的类名可能是.el-radio-button__inner,同样需要调整样式。 还需要注意父容器的宽度是否足够,如果父容器宽度受限,即使设置了换行,文本也无法正确换行。因此,可能需要同时设置.el-radio-group或外层容器的宽度。 最后,建议用户通过浏览器开发者工具检查元素,找到具体的CSS类名,并覆盖相应的样式。同时,测试在不同浏览器中的兼容性,确保解决方案有效。</think>在Element UI的`el-radio-group`中实现文字换行,需要通过CSS覆盖默认样式。以下是具体实现步骤: --- ### 解决方案 1. **调整标签样式** 在全局或组件作用域内添加CSS,修改`.el-radio__label`的`white-space`属性: ```css /* 普通radio选项 */ .el-radio__label { white-space: normal !important; /* 允许换行 */ word-break: break-all; /* 强制长单词换行 */ } /* Radio按钮模式 */ .el-radio-button__inner { white-space: normal !important; word-break: break-all; } ``` 2. **设置容器宽度** 确保`el-radio-group`或其父容器有足够宽度: ```html <el-radio-group v-model="value" style="width: 500px"> <el-radio label="这是一个非常长的标签文本需要换行显示"></el-radio> </el-radio-group> ``` 3. **自定义模板(高级)** 使用插槽自定义标签内容,手动添加换行: ```html <el-radio-group v-model="value"> <el-radio :label="option.value" v-for="option in options"> <span style="white-space: pre-line">{{ option.label }}</span> </el-radio> </el-radio-group> ``` --- ### 关键原理 - Element UI默认通过`white-space: nowrap`禁止换行[^3] - 需通过`!important`覆盖框架默认样式 - 按钮模式下需修改`.el-radio-button__inner`样式 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值