结合Vue3.0学习elementUi 源码
文章目录
前言
用vue3.0把elementui 的组件源码重写一遍,是个不错的学习vue3.0的方式,同时可以深入理解elementui中组件的实现方式
在重写之前当然要先创建一个vue3的项目,再把elementui的源码拿到分析一下要用到的文件有哪些,我这里就直接把组件的文件夹packages下的各组件,packages包里面有一个
theme-chalk的文件夹是存放各组件的样式的文件,这个我直接复制到项目中,在各个组件引入即可用了。
一、element ui 之 container
(一)对container的简单解析
首先看element ui 官网中的第二个组件container,这个组件由header、container、footer、aside各组件组成。
- header组件
header组件的文件结构
index.js文件, 文件内容用于注册文件,当vue实例调用use
方法时会自动调用install
方法
import Header from './src/main';
/* istanbul ignore next */
Header.install = function(Vue) {
Vue.component(Header.name, Header);
};
export default Header;
main.vue文件
标签
<slot></slot>
插槽,用于插入其他内容,props
接收组件传值,通过height
属性来改变内容高度
<template>
<header class="el-header" :style="{ height }">
<slot></slot>
</header>
</template>
<script>
export default {
name: 'ElHeader',
componentName: 'ElHeader',
props: {
height: {
type: String,
default: '60px'
}
}
};
</script>
- container组件
与header组件类似,的文件结构,index.js文件用于注册组件,main.js文件为组件内容
index.js
import Container from './src/main';
/* istanbul ignore next */
Container.install = function(Vue) {
Vue.component(Container.name, Container);
};
export default Container;
main.vue
direction
属性是用于设置组件中的内容布局方式,这个属性会控制样式中flex布局是row还是colum,当为vertical
时垂直排列horizontal
时水平排列,当出现标签为el-header
或el-footer
标签时,设置为垂直排列,其他情况为默认的水平排列
<template>
<section class="el-container" :class="{ 'is-vertical': isVertical }">
<slot></slot>
</section>
</template>
<script>
export default {
name: 'ElContainer',
componentName: 'ElContainer',
props: {
direction: String
},
computed: {
isVertical() {
if (this.direction === 'vertical') {
return true;
} else if (this.direction === 'horizontal') {
return false;
}
return this.$slots && this.$slots.default
? this.$slots.default.some(vnode => {
const tag = vnode.componentOptions && vnode.componentOptions.tag;
return tag === 'el-header' || tag === 'el-footer';
})
: false;
}
}
};
</script>
- footer组件
组件结构与上面header组件一样,index.js注册组件,main.vue组件内容,index.js文件不展示代码,直接看main.vue文件,height属性控制高度
main.vue
<template>
<footer class="el-footer" :style="{ height }">
<slot></slot>
</footer>
</template>
<script>
export default {
name: 'ElFooter',
componentName: 'ElFooter',
props: {
height: {
type: String,
default: '60px'
}
}
};
</script>
- aside组件
width属性设置aside组件的宽度
<template>
<aside class="el-aside" :style="{ width }">
<slot></slot>
</aside>
</template>
<script>
export default {
name: 'ElAside',
componentName: 'ElAside',
props: {
width: {
type: String,
default: '300px'
}
}
};
</script>
- header组件, container组件,footer组件,aside组件
vue3中注册组件
(二)、用Vue3.0的方式重写container
import Header from './src/index.vue';
export default {
install: (app) => {
app.component('el-header', Header);
}
}
header、footer、aside组件内容没有变,contaier组件内容有点变化,vue3中
setup()
会被立即执行,state
相当于vue2中的data
,vue3中通过reactive()
来实现响应式数据,或者ref()
来实现响应式,
setup()
返回state,就可以用于数据渲染了通过state.属性
的方式渲染
<template>
<section class="el-container" :class="{ 'is-vertical': state.isVertical }"></section>
<slot></slot>
</template>
<script>
import '../../plaginStyle/src/container.scss'; //引入组件样式
import {ref, reactive, computed} from 'vue'; // 引入接口
export default {
name: 'ElContainer',
componentName: 'ElContainer',
props: {
direction: String
},
setup(props, context) {
const state = reactive({
isVertical: computed(() => {
if (props.direction === 'vertical') {
return true;
} else {
return false;
}
return context.slots && context.slots.default
? this.slots.default.some(vnode => {
const tag = vnode.componentOptions && vnode.componentOptions.tag;
return tag === 'el-header' || tag === 'el-footer';
})
: false;
})
});
return {
state
}
}
}
</script>
(三)、container用vue3.0写的差异部分
与vue2不同的是
1、vue3通过apireactive()
或者ref()
来实现数据的响应,
关于这两个接口的详细解释composition Api
2、书写方式不一样,因为container部分,大部分都是通过样式控制的,与vue2除了写法上有不一样,其他实现都是一样的,vue3.0的具体书写方式的描写可以通过官网更详细的学习
二、element ui 之 layout
(一)、layout 简单解析
layout部分,是由row, col各组件组成。
- row组件
组件的文件结构和上面的container各组件是类似的,注册方式也是一样的,只是引用的组件和名称是各自的。这里直接看组件的内容
export default {
name: 'ElRow', // 组件名称
componentName: 'ElRow', // 组件名称
props: { // 接收的父组件传递的各个参数
tag: { // 自定义元素标签
type: String,
default: 'div'
},
gutter: Number, // 栅格间隔,每行中各小部分间隔
type: String, //布局模式,可选 flex,现代浏览器下有效
justify: { // felx布局下水平排列方式
type: String,
default: 'start'
},
align: { // felx布局下垂直布局方式
type: String,
default: 'top'
}
},
computed: {
style() {
const ret = {};
if (this.gutter) { // 设置左右两边间距为栅格间距的一半
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}
return ret;
}
},
render(h) { // 返回渲染
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}
};
与container部分不同的是它的内容是一个js文件,通过渲染函数动态渲染。
h()
方法的三个参数: 第一个,渲染时要创建的标签;第二个,该标签的所有要用到的属性;第三个,子元素可以是插槽对象
- col组件
组件的文件结构和上面的container各组件是类似的,注册方式也是一样的,只是引用的组件和名称是各自的。这里直接看组件的内容
export default {
name: 'ElCol', //组件名称
props: { // 接收父租价传参
span: { //栅格占据的列数 默认24为最大
type: Number,
default: 24
},
tag: { //自定义元素标签
type: String,
default: 'div'
},
offset: Number, // 栅格左侧的间隔格数
pull: Number, // 栅格向右移动格数
push: Number, //栅格向左移动格数
xs: [Number, Object], //<768px 响应式栅格数或者栅格属性对象
sm: [Number, Object], // ≥768px 响应式栅格数或者栅格属性对象
md: [Number, Object], // ≥992px 响应式栅格数或者栅格属性对象
lg: [Number, Object], // ≥1200px 响应式栅格数或者栅格属性对象
xl: [Number, Object] // ≥1920px 响应式栅格数或者栅格属性对象
},
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
// 遍历参数,将存在的参数对应的类添加到类数组中
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (this[prop] || this[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${this[prop]}`
: `el-col-${this[prop]}`
);
}
});
// 遍历参数,将存在的参数对应的类添加到类数组中
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof this[size] === 'number') {
classList.push(`el-col-${size}-${this[size]}`);
} else if (typeof this[size] === 'object') {
let props = this[size];
Object.keys(props).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${props[prop]}`
: `el-col-${size}-${props[prop]}`
);
});
}
});
// 渲染
return h(this.tag, {
class: ['el-col', classList],
style
}, this.$slots.default);
}
};
(二)、用vue3.0重写Layout
- row组件
直接看组件内容,
import '../../../plaginStyle/src/row.scss'; // 组件样式
import {ref, reactive, computed, h} from 'vue'; // api
export default {
name: 'ElRow',
componentName: 'ElRow',
props: { // 接收父组件的传参
tag: { // 自定义标签
type: String,
default: 'div'
},
gutter: Number, // 栅格间隔(左右)
type: String, // 样式布局类型
justify: { // flex布局下 子元素的水平排列方式
type: String,
default: 'start'
},
align: { // flex布局下 子元素的垂直排列方式
type: String,
default: 'top'
}
},
setup(props, context) {
const state = reactive({
style: computed(() => {
const ret = {};
if (props.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginRight;
}
return ret;
})
})
return () => h(props.tag, {
class: [
'el-row',
props.justify !== 'start' ? `is-justify-${props.justify}` : '',
props.align !== 'top' ? `is-align-${props.align}` : '',
{ 'el-row--flex': props.type === 'flex' }
],
style: state.style
}, context.slots);
}
};
代码逻辑是一样的,这里需要注意的是,没有
render()
函数了,而是通过setup()
直接返回h()
渲染。获取插槽的内容是通过setup()
的参数context获取。computed
计算属性是在reactive()
里定义
- col组件
直接看内容
import '../../../plaginStyle/src/col.scss';
import {ref, reactive, computed, h} from 'vue';
export default {
name: 'ElCol',
props: {
span: {
type: Number,
default: 24
},
tag: {
type: String,
default: 'div'
},
offset: Number,
pull: Number,
push: Number,
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},
setup(props, context) {
const state = reactive({
gutter: computed(() => {
let parent = context.parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
})
})
return () => {
let classList = [];
let style = {};
if (props.gutter) {
style.paddingLeft = props.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (props[prop] || props[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${props[prop]}`
: `el-col-${props[prop]}`
);
}
});
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof props[size] === 'number') {
classList.push(`el-col-${size}-${props[size]}`);
} else if (typeof props[size] === 'object') {
let pop = props[size];
Object.keys(pop).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${pop[prop]}`
: `el-col-${size}-${pop[prop]}`
);
});
}
});
return h(props.tag, {
class: ['el-col', classList],
style
}, context.slots);
};
}
}
col组件和row组件一样,逻辑实现没有改变,差异与row组件中的差异一样
(三)、layout用vue3写的差异部分
1、vue3中的render直接返回一个函数,函数的返回值就是h(),vue3的渲染具体解析可看
2、vue3中,$slots对象是通过context参数中获取context.slots
, setup()函数参数的具体解析
3、vue3中,computed属性是在reactive()
函数中定义的,方式:属性值:computed(() => { return value})
vue3 computed计算属性
4、vue3中用到的api需要从vue中引入
三、element ui 之 button
(一)、button组件的简单解析
button部分是由 button组件和button-group组件共同组成,
该部分的文件结构如下:
index文件是用于注册各组件的,方式和上面了解的一样,内容文件是.vue文件,通过名称可以知道对应的组件内容,直接看内容
- button组件
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
inject: { // 注入对象,表单对象时要用到
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
type: { // 按钮类型
type: String,
default: 'default'
},
size: String, //按钮尺寸
icon: { // 图标类名
type: String,
default: ''
},
nativeType: { // 原生 type 属性
type: String,
default: 'button'
},
loading: Boolean, // 是否加载状态
disabled: Boolean, //是否禁用状态
plain: Boolean, // 是否朴素按钮
autofocus: Boolean, // 是否聚焦
round: Boolean, //是否圆角
circle: Boolean // 是否圆形
},
computed: {
_elFormItemSize() { // 获取form表单对象的属性
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() { // 按钮的大小
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() { // 按钮是否禁用
return this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
handleClick(evt) { // 向父级传值,方法名称click
this.$emit('click', evt);
}
}
};
</script>
首先是template部分,这里面是一个button标签,button标签包裹两个i标签和一个span标签,第一i标签是指定loading图标,第二个是自定义图标,span标签是用于存放插槽内容的,也就是button的文字描述,提交或重置之类的。
其次是button中用到的属性,都在上面代码有注释
- button-group组件
<template>
<div class="el-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ElButtonGroup'
};
</script>
这个组件没有什么内容就是向该组件内部插入内容,可以有多个按钮
(二)、vue3重写button
文件结构和上面的一样,直接看内容
button组件
<template>
<button
class="el-button"
@click="handleClick"
:disabled="state.buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
state.buttonSize ? 'el-button--' + state.buttonSize : '',
{
'is-disabled': state.buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="context.slots"><slot></slot></span>
</button>
</template>
<script>
import '../../plaginStyle/src/button.scss'
import {ref, reactive, computed, watchEffect, onMounted} from 'vue';
export default {
name: 'ElButton',
props: {
type: { //按钮类型 primary / success / warning / danger / info / text
type: String,
default: 'default'
},
size: { // 按钮尺寸
type: String,
default: 'small'
},
icon: { // 按钮自定义的icon icon为class名称
type: String,
default: ''
},
nativeType: { // 按钮原生type属性 button / submit / reset
type: String,
default: 'button'
},
loading: Boolean, // loading的icon图标是否展示
disabled: Boolean, // 是否禁用按钮
plain: Boolean, // 是否朴素按钮
autofocus: Boolean, // 是否默认聚焦
round: Boolean, // 是否圆角按钮
circle: Boolean // 是否圆形按钮
},
setup(props, context) {
const state = reactive({
buttonSize: computed(() => {
return props.size
}),
buttonDisabled: computed(() => {
return props.disabled || false
}),
})
function handleClick(evt) {
context.emit('click', evt);
}
return {
handleClick,
state,
context
}
}
}
</script>
<style lang="scss" scoped>
</style>
button组件中与vue2的差异上面提到的都在这个组件体现了,需要注意的一点是,vue2中的$emit对象,在vue3中是通过context参数获取的
context.emit
,使用方式不变
button-group组件没有变化,这里不重复
(三)、vue3重新button组件差异
加上上面提到的差异,加上一条:子组件向父组件传值时,通过context来获取emit对象,详细解释上面有相关链接
四、element ui 之 link
(一)、组件的简单解析
直接看组件内容
<template>
<a
:class="[
'el-link',
type ? `el-link--${type}` : '',
disabled && 'is-disabled',
underline && !disabled && 'is-underline'
]"
:href="disabled ? null : href"
v-bind="$attrs"
@click="handleClick"
>
<i :class="icon" v-if="icon"></i>
<span v-if="$slots.default" class="el-link--inner">
<slot></slot>
</span>
<template v-if="$slots.icon"><slot v-if="$slots.icon" name="icon"></slot></template>
</a>
</template>
<script>
export default {
name: 'ElLink',
props: {
type: { // 链接类型 primary / success / warning / danger / info
type: String,
default: 'default'
},
underline: { // 是否带下滑线
type: Boolean,
default: true
},
disabled: Boolean, // 是否禁用
href: String, // 原生属性
icon: String // 图标类名
},
methods: {
handleClick(event) {
if (!this.disabled) {
if (!this.href) {
this.$emit('click', event);
}
}
}
}
};
</script>
link组件内容简单,在a标签中v-bind=“ a t t r s ” 他 的 作 用 是 包 含 了 父 作 用 域 中 不 作 为 组 件 p r o p s 或 自 定 义 事 件 。 当 一 个 组 件 没 有 声 明 任 何 p r o p 时 , 这 里 会 包 含 所 有 父 作 用 域 的 绑 定 , 并 且 可 以 通 过 v − b i n d = " attrs” 他的作用是 包含了父作用域中不作为组件 props 或自定义事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=" attrs”他的作用是包含了父作用域中不作为组件props或自定义事件。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定,并且可以通过v−bind="attrs" 传入内部组件——在创建高阶的组件时非常有用。
(二)、用vue3重写link
直接看内容
<template>
<a
:class="[
'el-link',
type ? `el-link--${type}` : '',
disabled && 'is-disabled',
underline && !disabled && 'is-underline'
]"
:href="disabled ? null : href"
v-bind="context.attrs"
@click="handleClick"
>
<i :class="icon" v-if="icon"></i>
<span v-if="context.slots.default" class="el-link--inner">
<slot></slot>
</span>
<template v-if="context.slots.icon"><slot v-if="context.slots.icon" name="icon"></slot></template>
</a>
</template>
<script>
import '../../../plaginStyle/src/link.scss';
import {ref, reactive, computed} from 'vue';
export default {
name: 'ElLink',
props: {
type: {
type: String,
default: 'default'
},
underline: {
type: Boolean,
default: true
},
disabled: Boolean,
herf: String,
icon: String
},
setup(props, context) {
function handleClick(event) {
if (!props.disabled) {
if (!props.herf) {
context.emit('click', event);
}
}
};
return {
context,
handleClick
};
}
}
</script>
实现逻辑是一样的,v-bind=“ a t t r s ” 的 实 现 方 式 是 一 样 的 , 只 要 把 " attrs”的实现方式是一样的,只要把" attrs”的实现方式是一样的,只要把"attrs"对象的获取方式改成context.attrs。
(三)、link组件用vue3写的差异
大部分差异的部分在上面已经有说明过,加上一点,vue2中的$attrs,在vue3
中是通过context参数来获取的,方式:context.attrs
五、element ui 之 radio组件
(一)、组件简单解析
radio组件是由,radio组件,radio-button组件、radio-group组件共同组成。
组件文件结构
下面看组件内容
radio组件
<template>
<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"
>
<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"
:value="label"
type="radio"
aria-hidden="true"
v-model="model"
@focus="focus = true"
@blur="focus = false"
@change="handleChange"
:name="name"
:disabled="isDisabled"
tabindex="-1"
>
</span>
<span class="el-radio__label" @keydown.stop>
<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: {}, // 涉及到了v-model里面语法糖的原理,接收的是radio值
label: {}, // 当前radio的值
disabled: Boolean, // 是否禁用
name: String, // 原生属性
border: Boolean, // 是否加边框
size: String // 尺寸大小
},
data() {
return {
focus: false
};
},
computed: {
isGroup() { // 判断radio组件的父组件,逐级向上直到找到为radio-group组件时,赋值_radioGroup为radio-group组件
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent;
return true;
}
}
return false;
},
model: { // v-model绑定的值,用于获取选中radio的值和设置raadio的值,实现双向的绑定
get() {
return this.isGroup ? this._radioGroup.value : this.value;
},
set(val) {
if (this.isGroup) {
this.dispatch('ElRadioGroup', 'input', [val]);
} else {
this.$emit('input', val);
}
this.$refs.radio && (this.$refs.radio.checked = this.model === this.label);
}
},
_elFormItemSize() { // 获取表单项的尺寸
return (this.elFormItem || {}).elFormItemSize;
},
radioSize() { // radio尺寸, 逐级向上取值
const temRadioSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
return this.isGroup
? this._radioGroup.radioGroupSize || temRadioSize
: temRadioSize;
},
isDisabled() { // 是否禁用
return this.isGroup
? this._radioGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
tabIndex() { // 这个和键盘上下键控制有关,不是很董
return (this.isDisabled || (this.isGroup && this.model !== this.label)) ? -1 : 0;
}
},
methods: {
handleChange() {
this.$nextTick(() => {
// 向父级传值,在radio值改变时触发
this.$emit('change', this.model);
this.isGroup && this.dispatch('ElRadioGroup', 'handleChange', this.model);
});
}
}
};
</script>
radio组件的标签结构组成:label标签下是一个span标签,到这里还看不到组件具体长啥样,span标签的里面包裹了第一个span标签(组件的圆点),第二个input(真正的原生radio,通过样式隐藏起来了),第三个span就是圆点后面关联的文字了,这里radio的样子才完全出来了,
radio组件的逻辑实现:
isGroup
方法就是逐级向上判断是否有radio-group组件,并且赋值该实例对象;
model
属性是与input中v-model绑定的,这里涉及到了vue ,v-model语法糖的原理,放上链接:https://juejin.cn/post/6844903662284718088
上面链接有详细的解释v-model的原理,这里简单说一下,v-model之所以会实时改变,是它本身的实现方式是,在input的触发input或change事件时执行改变当前input标签的值。
其他属性从名称上可以知道,具体的作用,
dispatch
方法是一个工具类方法,直接引入,这个方法的作用是向指定的组件派发方法传值,
radio-group组件
<template>
<component
:is="_elTag"
class="el-radio-group"
role="radiogroup"
@keydown="handleKeydown"
>
<slot></slot>
</component>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
});
export default {
name: 'ElRadioGroup',
componentName: 'ElRadioGroup',
inject: {
elFormItem: {
default: ''
}
},
mixins: [Emitter],
props: {
value: {},
size: String,
fill: String,
textColor: String,
disabled: Boolean
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
_elTag() {
return (this.$vnode.data || {}).tag || 'div';
},
radioGroupSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
}
},
created() {
this.$on('handleChange', value => {
this.$emit('change', value);
});
},
mounted() {
// 当radioGroup没有默认选项时,第一个可以选中Tab导航
const radios = this.$el.querySelectorAll('[type=radio]');
const firstLabel = this.$el.querySelectorAll('[role=radio]')[0];
if (![].some.call(radios, radio => radio.checked) && firstLabel) {
firstLabel.tabIndex = 0;
}
},
methods: {
handleKeydown(e) { // 左右上下按键 可以在radio组内切换不同选项
const target = e.target;
const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
const radios = this.$el.querySelectorAll(className);
const length = radios.length;
const index = [].indexOf.call(radios, target);
const roleRadios = this.$el.querySelectorAll('[role=radio]');
switch (e.keyCode) {
case keyCode.LEFT:
case keyCode.UP:
e.stopPropagation();
e.preventDefault();
if (index === 0) {
roleRadios[length - 1].click();
roleRadios[length - 1].focus();
} else {
roleRadios[index - 1].click();
roleRadios[index - 1].focus();
}
break;
case keyCode.RIGHT:
case keyCode.DOWN:
if (index === (length - 1)) {
e.stopPropagation();
e.preventDefault();
roleRadios[0].click();
roleRadios[0].focus();
} else {
roleRadios[index + 1].click();
roleRadios[index + 1].focus();
}
break;
default:
break;
}
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [this.value]);
}
}
};
</script>
radio-group组件的标签结构就是接收插入的内容,radio-group组件是在created方法中监听了子组件radio派发的
handleChange
方法后再向父组件派发chang方法,从而改变radio-group组件中v-model绑定的值,并实现了单项选择,
radio-button组件
<template>
<label
class="el-radio-button"
:class="[
size ? 'el-radio-button--' + size : '',
{ 'is-active': value === label },
{ 'is-disabled': isDisabled },
{ 'is-focus': focus }
]"
role="radio"
:aria-checked="value === label"
:aria-disabled="isDisabled"
:tabindex="tabIndex"
@keydown.space.stop.prevent="value = isDisabled ? value : label"
>
<input
class="el-radio-button__orig-radio"
:value="label"
type="radio"
v-model="value"
:name="name"
@change="handleChange"
:disabled="isDisabled"
tabindex="-1"
@focus="focus = true"
@blur="focus = false"
>
<span
class="el-radio-button__inner"
:style="value === label ? activeStyle : null"
@keydown.stop>
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElRadioButton',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
label: {},
disabled: Boolean,
name: String
},
data() {
return {
focus: false
};
},
computed: {
value: {
get() {
return this._radioGroup.value;
},
set(value) {
this._radioGroup.$emit('input', value);
}
},
_radioGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
return parent;
}
}
return false;
},
activeStyle() {
return {
backgroundColor: this._radioGroup.fill || '',
borderColor: this._radioGroup.fill || '',
boxShadow: this._radioGroup.fill ? `-1px 0 0 0 ${this._radioGroup.fill}` : '',
color: this._radioGroup.textColor || ''
};
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
size() {
return this._radioGroup.radioGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
isDisabled() {
return this.disabled || this._radioGroup.disabled || (this.elForm || {}).disabled;
},
tabIndex() {
return (this.isDisabled || (this._radioGroup && this.value !== this.label)) ? -1 : 0;
}
},
methods: {
handleChange() {
this.$nextTick(() => {
this.dispatch('ElRadioGroup', 'handleChange', this.value);
});
}
}
};
</script>
radio-button组件的标签结构很像,唯一不同的是没有了圆点,
实现逻辑上基本一致,唯一不同的是,它只能和radio-group组合使用,它不能单独向父级传值,必须通过radio-group这一层
(二)、vue3 radio组件重写
radio组件
<template>
<label
class="el-radio"
:class="[
border && state.radioSize ? 'el-radio--' + state.radioSize : '',
{ 'is-disabled': state.isDisabled },
{ 'is-focus': state.focus },
{ 'is-bordered': border },
{ 'is-checked': state.model === label }
]"
role="radio"
:aria-checked="state.model === label"
:aria-disabled="state.isDisabled"
:tabindex="state.tabIndex"
@keydown.space.stop.prevent="state.model = state.isDisabled ? state.model : label"
>
<span class="el-radio__input"
:class="{
'is-disabled': state.isDisabled,
'is-checked': state.model === label
}"
>
<span class="el-radio__inner"></span>
<input
ref="radio"
class="el-radio__original"
:value="label"
type="radio"
aria-hidden="true"
v-model="state.model"
@focus="state.focus = true"
@blur="state.focus = false"
@change.stop.prevent="handleChange"
:name="name"
:disabled="state.isDisabled"
tabindex="-1"
>
</span>
<span class="el-radio__label" @keydown.stop>
<slot></slot>
<template v-if="!context.slot">{{label}}</template>
</span>
</label>
</template>
<script>
import '../../../plaginStyle/src/radio.scss'
import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import stepVue from '../../../../../element-dev/packages/steps/src/step.vue';
export default {
name: 'ElRadio',
componentName: 'ElRadio',
props: {
modelValue: {},
label: {},
disabled: Boolean,
name: String,
border: Boolean,
size: String
},
setup(props, context) {
const currentInstance = getCurrentInstance();
const {dispatch, broadcast} = Emitter(currentInstance);
const radio = ref(null);
const state = reactive({
focus: false,
_radioGroup: null,
isGroup: computed(() => {
let parent = currentInstance.parent;
while (parent) {
if (parent.type.componentName !== 'ElRadioGroup') {
parent = parent.parent;
} else {
state._radioGroup = parent;
return true;
}
}
return false;
}),
model: computed({
get: () => {
return state.isGroup ? state._radioGroup.props.modelValue : props.modelValue
},
set: (val) => {
if (state.isGroup) {
dispatch('ElRadioGroup', 'update:modelValue', [val]);
} else {
context.emit('update:modelValue', val);
}
radio.value && (radio.value.checked = state.model === props.label)
}
}),
_elFormItemSize: computed(() => {
return '';
}),
radioSize: computed(() => {
const temRadioSize = props.size;
return state.isGroup
? state._radioGroup.props.radioGroupSize || temRadioSize
: temRadioSize;
}),
isDisabled: computed(() => {
return state.isGroup
? state._radioGroup.props.disabled || props.disabled
: props.disabled;
}),
tabIndex: computed(() => {
return (state.isDisabled || (state.isGroup && state.model !== props.label)) ? -1 : 0;
})
});
onMounted(() => {
})
function handleChange() {
context.emit('change', state.model);
state.isGroup && dispatch('ElRadioGroup', 'change', state.model);
}
return {
state,
handleChange,
context,
radio
}
}
}
</script>
标签结构和vue2是一样的,主要是里面的渲染数据写法不一样
逻辑实现是一样的,区别在于里面有些对象,获取的方式不同,
1、isGroup
是通过currentInstance获取父级对象的,currentInstance对象是再setup方法里面被定义,它是从vue中引入的getCurrentInstance()方法获取,currentInstance相当于vue2中的this,
2、model
这个和vue2,中v-model的原理是一样的,区别在vue2中获取选中值得value要用modelValue
名,向父级传值时得方法change要改成update:modelValue
参考链接:https://v3.cn.vuejs.org/guide/component-basics.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model
radio-group组件
<template>
<component
:is="state._elTag"
class="el-radio-group"
role="radiogroup"
@keydown="handleKeydown"
>
<slot></slot>
</component>
</template>
<script>
import '../../../plaginStyle/src/radio-group.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {
ref,
reactive,
computed,
getCurrentInstance,
onMounted,
watch,
inject
} from 'vue';
const keyCode = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40
})
export default {
name: 'ElRadioGroup',
componentName: 'ElRadioGroup',
props: {
modelValue: {},
size: String,
fill: String,
textColor: String,
disabled: Boolean
},
setup(props, context) {
const elFormItem = inject(elFormItem, ref(''));
const currentInstance = getCurrentInstance();
const {ctx} = getCurrentInstance();
const {dispatch} = Emitter(currentInstance);
const state = reactive({
_elFormItemSize: computed(() => {
return (elFormItem || {}).elFormItemSize;
}),
_elTag: computed(() => {
return (currentInstance.vnode.data || {}).tag || 'div';
}),
radioGroupSize: computed(() => {
return props.size || state._elFormItemSize
})
});
onMounted(() => {
const radios = currentInstance.vnode.el.querySelectorAll('[type=radio]');
const firstLabel = currentInstance.vnode.el.querySelectorAll('[type=radio]')[0];
if (![].some.call(radios, radio => radio.checked) && firstLabel) {
firstLabel.tabIndex = 0;
}
});
watch(() => props.modelValue, (modelValue, preModelValue) => {
dispatch('ElFormItem', 'el.form.change', [props.modelValue]);
})
function handleKeydown(e) {
const target = e.target;
const className = target.nodeName === 'INPUT' ? '[type=radio]' : '[role=radio]';
const radios = currentInstance.vnode.el.querySelectorAll(className);
const length = radios.length;
const index = [].indexOf.call(radios, target);
const roleRadios = currentInstance.vnode.el.querySelectorAll('[role=radio]');
switch (e.keyCode) {
case keyCode.LEFT:
case keyCode.UP:
e.stopPropagation();
e.preventDefault();
if (index === 0) {
roleRadios[length - 1].click();
roleRadios[length - 1].focus();
} else {
roleRadios[index - 1].click();
roleRadios[index - 1].focus();
}
break;
case keyCode.RIGHT:
case keyCode.DOWN:
if (index === (length - 1)) {
e.stopPropagation();
e.preventDefault();
roleRadios[0].click();
roleRadios[0].focus();
} else {
roleRadios[index + 1].click();
roleRadios[index + 1].focus();
}
break;
default:
break;
}
};
return {
state,
handleKeydown
}
}
}
</script>
radio-button组件
<template>
<label
class="el-radio-button"
:class="[
state.size ? 'el-radio-button--' + state.size : '',
{ 'is-active': state.modelValue === label },
{ 'is-disabled': state.isDisabled },
{ 'is-focus': state.focus }
]"
role="radio"
:aria-checked="state.modelValue === label"
:aria-disabled="state.isDisabled"
:tabindex="state.tabIndex"
@keydown.space.stop.prevent="state.modelValue = state.isDisabled ? state.modelValue : label"
>
<input
class="el-radio-button__orig-radio"
:value="label"
type="radio"
v-model="state.modelValue"
:name="name"
@change.stop="handleChange"
:disabled="state.isDisabled"
tabindex="-1"
@focus="state.focus = true"
@blur="state.focus = false"
>
<span
class="el-radio-button__inner"
:style="state.modelValue === label ? state.activeStyle : null"
@keydown.stop>
<slot></slot>
<template v-if="!context.slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import '../../../plaginStyle/src/radio-button.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {
ref,
reactive,
computed,
getCurrentInstance,
inject,
onMounted
} from 'vue';
export default {
name: 'ElRadioButton',
props: {
label: {},
disabled: Boolean,
name: String
},
setup(props, context) {
const elForm = inject(elForm, ref(''));
const elFormItem = inject(elFormItem, ref(''));
const currentInstance = getCurrentInstance();
const {dispatch} = Emitter(currentInstance);
const state = reactive({
focus: false,
modelValue: computed({
get: () => {
return state._radioGroup.props.modelValue;
},
set: (value) => {
state._radioGroup.emit('update:modelValue', value);
}
}),
_radioGroup: computed(() => {
let parent = currentInstance.parent;
while (parent) {
if (parent.type.componentName !== 'ElRadioGroup') {
parent = parent.parent;
} else {
return parent;
}
}
return false;
}),
activeStyle: computed(() => {
return {
backgroundColor: state._radioGroup.ctx.fill || '',
borderColor: state._radioGroup.ctx.fill || '',
boxShadow: state._radioGroup.ctx.fill ? `-1px 0 0 0 ${state._radioGroup.ctx.fill}` : '',
color: state._radioGroup.ctx.textColor || ''
};
}),
_elFormItemSize: computed(() => {
return (elFormItem || {}).elFormItemSize;
}),
size: computed(() => {
return state._radioGroup.ctx.radioGroupSize || state._elFormItemSize
}),
isDisabled: computed(() => {
return props.disabled || state._radioGroup.ctx.disabled || (state.elForm || {}).disabled;
}),
tabIndex: computed(() => {
return (state.isDisabled || (state._radioGroup && state.modelValue !== props.label)) ? -1 : 0;
})
});
function handleChange() {
dispatch('ElRadioGroup', 'change', state.modelValue);
}
return {
state,
context,
handleChange
}
}
}
</script>
(三)、差异
1、vue3获取当前组件实例对象,不再是this,而是通过api getCurrentInstance方法获取,得到的对象和this会有区别,
2、在组件上使用v-model时,其语法上有区别,上面链接有详细解释
六、element ui 之 checkbox
(一)、checkbox简单解析
checkbox部分由checkbox、checkbox-button、checkbox-group组件构成
文件结构,和button组件的结构一样,
再来看各组件的具体内容
checkbox组件
<template>
<label
class="el-checkbox"
:class="[
border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
{ 'is-disabled': isDisabled },
{ 'is-bordered': border },
{ 'is-checked': isChecked }
]"
:id="id"
>
<span class="el-checkbox__input"
:class="{
'is-disabled': isDisabled,
'is-checked': isChecked,
'is-indeterminate': indeterminate,
'is-focus': focus
}"
:tabindex="indeterminate ? 0 : false"
:role="indeterminate ? 'checkbox' : false"
:aria-checked="indeterminate ? 'mixed' : false"
>
<span class="el-checkbox__inner"></span>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox__original"
type="checkbox"
:aria-hidden="indeterminate ? 'true' : 'false'"
:name="name"
:disabled="isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<input
v-else
class="el-checkbox__original"
type="checkbox"
:aria-hidden="indeterminate ? 'true' : 'false'"
:disabled="isDisabled"
:value="label"
:name="name"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
</span>
<span class="el-checkbox__label" v-if="$slots.default || 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: 'ElCheckbox',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
componentName: 'ElCheckbox',
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false
};
},
computed: {
model: {
get() {
return this.isGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this.isGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else {
this.$emit('input', val);
this.selfModel = val;
}
}
},
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
isGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElCheckboxGroup') {
parent = parent.$parent;
} else {
this._checkboxGroup = parent;
return true;
}
}
return false;
},
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
/* used to make the isDisabled judgment under max/min props */
isLimitDisabled() {
const { max, min } = this._checkboxGroup;
return !!(max || min) &&
(this.model.length >= max && !this.isChecked) ||
(this.model.length <= min && this.isChecked);
},
isDisabled() {
return this.isGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
: this.disabled || (this.elForm || {}).disabled;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
checkboxSize() {
const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
return this.isGroup
? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
: temCheckboxSize;
}
},
props: {
value: {},
label: {},
indeterminate: Boolean,
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number],
id: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
controls: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
border: Boolean,
size: String
},
methods: {
addToStore() {
if (
Array.isArray(this.model) &&
this.model.indexOf(this.label) === -1
) {
this.model.push(this.label);
} else {
this.model = this.trueLabel || true;
}
},
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this.isGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
},
created() {
this.checked && this.addToStore();
},
mounted() { // 为indeterminate元素 添加aria-controls 属性
if (this.indeterminate) {
this.$el.setAttribute('aria-controls', this.controls);
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', value);
}
}
};
</script>
他和button组件的实现方式很像
checkbox-group组件
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckboxGroup',
componentName: 'ElCheckboxGroup',
mixins: [Emitter],
inject: {
elFormItem: {
default: ''
}
},
props: {
value: {},
disabled: Boolean,
min: Number,
max: Number,
size: String,
fill: String,
textColor: String
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
checkboxGroupSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [value]);
}
}
};
</script>
<template>
<div class="el-checkbox-group" role="group" aria-label="checkbox-group">
<slot></slot>
</div>
</template>
checkbox-button组件
<template>
<label
class="el-checkbox-button"
:class="[
size ? 'el-checkbox-button--' + size : '',
{ 'is-disabled': isDisabled },
{ 'is-checked': isChecked },
{ 'is-focus': focus },
]"
role="checkbox"
:aria-checked="isChecked"
:aria-disabled="isDisabled"
>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<input
v-else
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="isDisabled"
:value="label"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<span class="el-checkbox-button__inner"
v-if="$slots.default || label"
:style="isChecked ? activeStyle : null">
<slot>{{label}}</slot>
</span>
</label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckboxButton',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false
};
},
props: {
value: {},
label: {},
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number]
},
computed: {
model: {
get() {
return this._checkboxGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this._checkboxGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else if (this.value !== undefined) {
this.$emit('input', val);
} else {
this.selfModel = val;
}
}
},
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
_checkboxGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElCheckboxGroup') {
parent = parent.$parent;
} else {
return parent;
}
}
return false;
},
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
activeStyle() {
return {
backgroundColor: this._checkboxGroup.fill || '',
borderColor: this._checkboxGroup.fill || '',
color: this._checkboxGroup.textColor || '',
'box-shadow': '-1px 0 0 0 ' + this._checkboxGroup.fill
};
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
size() {
return this._checkboxGroup.checkboxGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
/* used to make the isDisabled judgment under max/min props */
isLimitDisabled() {
const { max, min } = this._checkboxGroup;
return !!(max || min) &&
(this.model.length >= max && !this.isChecked) ||
(this.model.length <= min && this.isChecked);
},
isDisabled() {
return this._checkboxGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
: this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
addToStore() {
if (
Array.isArray(this.model) &&
this.model.indexOf(this.label) === -1
) {
this.model.push(this.label);
} else {
this.model = this.trueLabel || true;
}
},
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this._checkboxGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
},
created() {
this.checked && this.addToStore();
}
};
</script>
(二)、vue3重写checkbox
checkbox组件
<template>
<label
class="el-checkbox"
:class="[
border && state.checkboxSize ? 'el-checkbox--' + state.checkboxSize : '',
{'is-disabled': state.isDisabled},
{'is-bordered': border},
{'is-checked' : state.isChecked}
]"
:id="id"
>
<span class="el-checkbox__input"
:class="{
'is-disabled': state.isDisabled,
'is-checked': state.isChecked,
'is-indeterminate': indeterminate,
'is-focus': state.focus
}"
:tabindex="state.indeterminate ? 0 : false"
:role="state.indeterminate ? 'checkbox' : false"
:aria-checked="state.indeterminate ? 'mixed' : false"
>
<span class="el-checkbox__inner"></span>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox__original"
type="checkbox"
:aria-hidden="state.indeterminate ? 'true' : 'false'"
:name="name"
:disabled="state.isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="state.model"
@change.stop="handleChange"
@focus="state.focus = true"
@blur="state.focus = false">
<input
v-else
class="el-checkbox__original"
type="checkbox"
:aria-hidden="state.indeterminate ? 'true' : 'false'"
:disabled="state.isDisabled"
:value="label"
:name="name"
v-model="state.model"
@change.stop="handleChange"
@focus="state.focus = true"
@blur="state.focus = false">
</span>
<span class="el-checkbox__label" v-if="context.slots.default || label">
<slot></slot>
<template v-if="!context.slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import '../../../plaginStyle/src/checkbox.scss';
import {ref, reactive, computed, onMounted, getCurrentInstance, watch} from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
// import tree from '../../../../../element-dev/packages/table/src/store/tree.js';
export default {
name: 'ElCheckbox',
componentName: 'ElCheckbox',
props: {
modelValue: {},
label: {},
indeterminate: Boolean,
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number],
id: String,
controls: String,
border: Boolean,
size: String
},
setup(props, context) {
const currentInstance = getCurrentInstance(); // 当前组件对象
const {dispatch, broadcast} = Emitter(currentInstance); // 派发方法
const state = reactive({
selfModel: false,
focus: false,
isLimitExceeded: false,
model: computed({ // v-model绑定值
get() {
return state.isGroup
? state.store : props.modelValue !== undefined
? props.modelValue : state.selfModel;
},
set(val) {
if (state.isGroup) {
state.isLimitExceeded = false;
(state.isGroup.ctx.min !== undefined &&
val.length < state.isGroup.ctx.min &&
(state.isLimitExceeded = true));
(state.isGroup.ctx.max !== undefined &&
val.length > state.isGroup.ctx.max &&
(state.isLimitExceeded = true));
state.isLimitExceeded === false &&
dispatch('ElCheckboxGroup', 'update:modelValue', [val]);
} else {
context.emit('update:modelValue', val);
state.selfModel = val;
}
}
}),
// 判断是否选中
isChecked: computed(() => {
if ({}.toString.call(state.model) === '[object Boolean]') {
return state.model;
} else if (Array.isArray(state.model)) {
return state.model.indexOf(props.label) > -1;
} else if (state.model !== null && state.model !== undefined) {
return state.model === props.trueLabel;
}
}),
isGroup: computed(() => {
let parent = currentInstance.parent;
while (parent) {
if (parent.type.componentName !== 'ElCheckboxGroup') {
parent = parent.parent;
} else {
// state._checkboxGroup = parent;
return parent;
}
}
return false;
}),
store: computed(() => {
return state.isGroup ? state.isGroup.ctx.modelValue : props.modelValue;
}),
isLimitDisabled: computed(() => {
const { max, min } = state.isGroup;
return !!(max || min) &&
(state.model.length >= max && !state.isChecked) ||
(state.model.length <= min && state.isChecked);
}),
isDisabled: computed(() => {
return state.isGroup
? state.isGroup.ctx.disabled || props.disabled || state.isLimitDisabled
: props.disabled;
}),
_elFormItemSize: computed(() => {
return (state.elFormItem || {}).elFormItemSize;
}),
checkboxSize: computed(() => {
const temCheckboxSize = props.size || state._elFormItemSize
return state.isGroup
? state.isGroup.ctx.checkboxGroupSize || temCheckboxSize
: temCheckboxSize;
}),
});
function addToStore() {
if (
Array.isArray(state.model) &&
state.model.indexOf(props.label) === -1
) {
state.model.push(props.label);
} else {
state.model = props.trueLabel || true;
}
}
function handleChange(ev) {
if (state.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = props.trueLabel === undefined ? true : props.trueLabel;
} else {
value = props.falseLabel === undefined ? false : props.falseLabel;
}
context.emit('change', value, ev);
if (state.isGroup) {
dispatch('ElCheckboxGroup', 'change', [state.isGroup.ctx.modelValue]);
}
}
// 首次执行相当于created
props.checked && addToStore();
// mounted
onMounted(() => {
if (state.indeterminate) {
currentInstance.vnode.setAttribute('aria-controls', this.controls);
}
});
watch(() => props.modelValue, (modelValue, preModelValue) => {
dispatch('ElFormItem', 'el.form.change', modelValue);
})
return {
state,
context,
addToStore,
handleChange
}
}
}
</script>
checkbox-group组件
<script>
import '../../../plaginStyle/src/checkbox-group.scss';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import {ref, reactive, computed, onMounted, watch, inject, getCurrentInstance} from 'vue';
export default {
name: 'ElCheckboxGroup',
componentName: 'ElCheckboxGroup',
props: {
modelValue: {},
disabled: Boolean,
min: Number,
max: Number,
size: String,
fill: String,
textColor: String
},
setup(props, context) {
const elFormItem = inject(elFormItem, ref(''));
const currentInstance = getCurrentInstance();
const {dispatch, broadcast} = Emitter(currentInstance);
const state = reactive({
_elFormItemSize: computed(() => {
return (elFormItem || {}).elFormItemSize;
}),
checkboxGroupSize: computed(() => {
return props.size || state._elFormItemSize
})
})
watch(() => props.modelValue, (modelValue, perModelValue) => {
dispatch('ElFormItem', 'el.form.change', [modelValue]);
})
}
}
</script>
<template>
<div class="el-checkbox-group" role="group" aria-label="checkbox-group">
<slot></slot>
</div>
</template>
checkbox-button组件
<template>
<label
class="el-checkbox-button"
:class="[
state.size ? 'el-checkbox-button--' + state.size : '',
{ 'is-disabled': state.isDisabled },
{ 'is-checked': state.isChecked },
{ 'is-focus': state.focus },
]"
role="checkbox"
:aria-checked="state.isChecked"
:aria-disabled="state.isDisabled"
>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="state.isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="state.model"
@change.stop="handleChange"
@focus="state.focus = true"
@blur="state.focus = false">
<input
v-else
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="state.isDisabled"
:value="label"
v-model="state.model"
@change.stop="handleChange"
@focus="state.focus = true"
@blur="state.focus = false">
<span class="el-checkbox-button__inner"
v-if="context.slots.default || label"
:style="state.isChecked ? state.activeStyle : null">
<slot>{{label}}</slot>
</span>
</label>
</template>
<script>
import '../../../plaginStyle/src/checkbox-button.scss';
import {ref, reactive, inject, computed, onMounted, watch, getCurrentInstance} from 'vue';
import Vue from 'vue';
import Emitter from '../../../plaginSrc/mixins/emitter.js';
import { props } from '../../../../../vue源码/vue-dev/test/weex/cases/recycle-list/components/counter.vue';
export default {
name: 'ElCheckboxButton',
props: {
modelValue: {},
label: {},
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number]
},
setup(props, context) {
const currentInstance = getCurrentInstance();
const {dispatch, broadcast} = Emitter(currentInstance);
const elForm = inject(elForm, ref(''));
const elFormItem = inject(elFormItem, ref(''));
const state = reactive({
selfModel: false,
focus: false,
isLimitExceeded: false,
model: computed({
get() {
return state._checkboxGroup
? state.store : props.modelValue !== undefined
? props.modelValue : state.selfModel;
},
set(val) {
if (state._checkboxGroup) {
state.isLimitExceeded = false;
(state._checkboxGroup.ctx.min !== undefined &&
val.length < state._checkboxGroup.ctx.min &&
(state.isLimitExceeded = true));
(state._checkboxGroup.ctx.max !== undefined &&
val.length > state._checkboxGroup.ctx.max &&
(state.isLimitExceeded = true));
state.isLimitExceeded === false &&
dispatch('ElCheckboxGroup', 'update:modelValue', [val]);
} else if (state.modelValue !== undefined) {
context.emit('update:modelValue', val);
} else {
state.selfModel = val;
}
}
}),
isChecked: computed(() => {
if ({}.toString.call(state.model) === '[object Boolean]') {
return state.model;
} else if (Array.isArray(state.model)) {
return state.model.indexOf(props.label) > -1;
} else if (state.model !== null && state.model !== undefined) {
return state.model = props.trueLabel;
}
}),
_checkboxGroup: computed(() => {
let parent = currentInstance.parent;
while (parent) {
if (parent.type.componentName !== 'ElCheckboxGroup') {
parent = parent.parent;
} else {
return parent;
}
}
return false;
}),
store: computed(() => {
return state._checkboxGroup ? state._checkboxGroup.ctx.modelValue : props.modelValue;
}),
activeStyle: computed(() => {
return {
backgroundColor: state._checkboxGroup.ctx.fill || '',
borderColor: state._checkboxGroup.ctx.fill || '',
color: state._checkboxGroup.ctx.textColor || '',
'box-shadow': '-1px 0 0 0' + state._checkboxGroup.ctx.fill
};
}),
_elFormItemSize: computed(() => {
return (elFormItem || {}).elFormItemSize;
}),
size: computed(() => {
return state._checkboxGroup.ctx.checkboxGroupSize || state._elFormItemSize;
}),
isLimitDisabled: computed(() => {
const { max, min } = state._checkboxGroup;
return !!(max || min) &&
(state.model.length >= max && !state.isChecked) ||
(state.model.length <= min && state.isChecked);
}),
isDisabled: computed(() => {
return state._checkboxGroup
? state._checkboxGroup.ctx.disabled || props.disabled || (elForm || {}).disabled || state.isLimitDisabled
: props.disabled || (elForm || {}).disabled;
}),
});
function addToStore() {
if (
Array.isArray(state.model) &&
state.model.indexOf(props.label) === -1
) {
state.model.push(props.label);
} else {
state.model = props.trueLabel || true;
}
}
function handleChange(ev) {
if (state.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = props.trueLabel === undefined ? true : props.trueLabel;
} else {
value = props.falseLabel === undefined ? false : props.falseLabel;
}
currentInstance.emit('change', value, ev);
if (state._checkboxGroup) {
dispatch('ElCheckboxGroup', 'change', [state._checkboxGroup.ctx.modelValue]);
}
}
props.checked && addToStore();
return {
state,
context,
currentInstance,
handleChange
}
}
}
</script>
七、element ui 之 input
(一)、源码
源码
<template>
<div :class="[
type === 'textarea' ? 'el-textarea' : 'el-input',
inputSize ? 'el-input--' + inputSize : '',
{
'is-disabled': inputDisabled,
'is-exceed': inputExceed,
'el-input-group': $slots.prepend || $slots.append,
'el-input-group--append': $slots.append,
'el-input-group--prepend': $slots.prepend,
'el-input--prefix': $slots.prefix || prefixIcon,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
}
]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</div>
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
ref="input"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
<!-- 前置内容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
<slot name="prefix"></slot>
<i class="el-input__icon"
v-if="prefixIcon"
:class="prefixIcon">
</i>
</span>
<!-- 后置内容 -->
<span
class="el-input__suffix"
v-if="getSuffixVisible()">
<span class="el-input__suffix-inner">
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon">
</i>
</template>
<i v-if="showClear"
class="el-input__icon el-icon-circle-close el-input__clear"
@mousedown.prevent
@click="clear"
></i>
<i v-if="showPwdVisible"
class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible"
></i>
<span v-if="isWordLimitVisible" class="el-input__count">
<span class="el-input__count-inner">
{{ textLength }}/{{ upperLimit }}
</span>
</span>
</span>
<i class="el-input__icon"
v-if="validateState"
:class="['el-input__validateIcon', validateIcon]">
</i>
</span>
<!-- 后置元素 -->
<div class="el-input-group__append" v-if="$slots.append">
<slot name="append"></slot>
</div>
</template>
<textarea
v-else
:tabindex="tabindex"
class="el-textarea__inner"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
ref="textarea"
v-bind="$attrs"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:style="textareaStyle"
@focus="handleFocus"
@blur="handleBlur"
@change.stop="handleChange"
:aria-label="label"
>
</textarea>
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span>
</div>
</template>
<script>
import emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import calcTextareaHeight from './calcTextareaHeight';
import merge from 'element-ui/src/utils/merge';
import {isKorean} from 'element-ui/src/utils/shared';
export default {
name: 'ElInput',
componentName: 'ElInput',
mixins: [emitter, Migrating],
inheritAttrs: false,
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
data() {
return {
textareaCalcStyle: {},
hovering: false,
focused: false,
isComposing: false,
passwordVisible: false
};
},
props: {
value: [String, Number],
size: String,
resize: String,
form: String,
disabled: Boolean,
readonly: Boolean,
type: {
type: String,
default: 'text'
},
autosize: {
type: [Boolean, Object],
default: false
},
autocomplete: {
type: String,
default: 'off'
},
/** @Deprecated in next major version */
autoComplete: {
type: String,
validator(val) {
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
return true;
}
},
validateEvent: {
type: Boolean,
default: true
},
suffixIcon: String,
prefixIcon: String,
label: String,
clearable: {
type: Boolean,
default: false
},
showPassword: {
type: Boolean,
default: false
},
showWordLimit: {
type: Boolean,
default: false
},
tabindex: String
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
validateState() {
return this.elFormItem ? this.elFormItem.validateState : '';
},
needStatusIcon() {
return this.elForm ? this.elForm.statusIcon : false;
},
validateIcon() {
return {
validating: 'el-icon-loading',
success: 'el-icon-circle-check',
error: 'el-icon-circle-close'
}[this.validateState];
},
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
nativeInputValue() {
return this.value === null || this.value === undefined ? '' : String(this.value);
},
showClear() {
return this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.nativeInputValue &&
(this.focused || this.hovering);
},
showPwdVisible() {
return this.showPassword &&
!this.inputDisabled &&
!this.readonly &&
(!!this.nativeInputValue || this.focused);
},
isWordLimitVisible() {
return this.showWordLimit &&
this.$attrs.maxlength &&
(this.type === 'text' || this.type === 'textarea') &&
!this.inputDisabled &&
!this.readonly &&
!this.showPassword;
},
upperLimit() {
return this.$attrs.maxlength;
},
textLength() {
if (typeof this.value === 'number') {
return String(this.value).length;
}
return (this.value || '').length;
},
inputExceed() {
// show exceed style if length of initial value greater then maxlength
return this.isWordLimitVisible &&
(this.textLength > this.upperLimit);
}
},
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
},
// native input value is set explicitly
// do not use v-model / :value in template
// see: https://github.com/ElemeFE/element/issues/14521
nativeInputValue() {
this.setNativeInputValue();
},
// when change between <input> and <textarea>,
// update DOM dependent value and styles
// https://github.com/ElemeFE/element/issues/14857
type() {
this.$nextTick(() => {
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
});
}
},
methods: {
focus() {
this.getInput().focus();
},
blur() {
this.getInput().blur();
},
getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
};
},
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
}
},
select() {
this.getInput().select();
},
resizeTextarea() {
if (this.$isServer) return;
const { autosize, type } = this;
if (type !== 'textarea') return;
if (!autosize) {
this.textareaCalcStyle = {
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
setNativeInputValue() {
const input = this.getInput();
console.log(input, 'input');
if (!input) return;
if (input.value === this.nativeInputValue) return;
input.value = this.nativeInputValue;
},
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
handleCompositionStart() {
this.isComposing = true;
},
handleCompositionUpdate(event) {
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
this.isComposing = !isKorean(lastCharacter);
},
handleCompositionEnd(event) {
if (this.isComposing) {
this.isComposing = false;
this.handleInput(event);
}
},
handleInput(event) {
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (this.isComposing) return;
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (event.target.value === this.nativeInputValue) return;
this.$emit('input', event.target.value);
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
this.$nextTick(this.setNativeInputValue);
},
handleChange(event) {
this.$emit('change', event.target.value);
},
calcIconOffset(place) {
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === this.$el) {
el = elList[i];
break;
}
}
if (!el) return;
const pendantMap = {
suffix: 'append',
prefix: 'prepend'
};
const pendant = pendantMap[place];
if (this.$slots[pendant]) {
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
} else {
el.removeAttribute('style');
}
},
updateIconOffset() {
this.calcIconOffset('prefix');
this.calcIconOffset('suffix');
},
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.focus();
},
getInput() {
return this.$refs.input || this.$refs.textarea;
},
getSuffixVisible() {
return this.$slots.suffix ||
this.suffixIcon ||
this.showClear ||
this.showPassword ||
this.isWordLimitVisible ||
(this.validateState && this.needStatusIcon);
}
},
created() {
this.$on('inputSelect', this.select);
},
mounted() {
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
},
updated() {
this.$nextTick(this.updateIconOffset);
}
};
</script>
(二)、vue3重写input
<template>
<div :class="[
type === 'textarea' ? 'el-textarea' : 'el-input',
state.inputSize ? 'el-input--' + state.inputSize : '',
{
'is-disabled': state.inputDisabled,
'is-exceed': state.inputExceed,
'el-input-group': context.slots.prepend || context.slots.append,
'el-input-group--append': context.slots.append,
'el-input-group--prepend': context.slots.prepend,
'el-input--prefix': context.slots.prefix || prefixIcon,
'el-input--suffix': context.slots.suffix || suffixIcon || clearable || showPassword
}
]"
@mouseenter="state.hovering = true"
@mouseleave="state.hovering = false"
>
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="context.slots.prepend">
<slot name="prepend"></slot>
</div>
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="context.attrs"
:type="showPassword ? (state.passwordVisible ? 'text' : 'password') : type"
:disabled="state.inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
ref="input"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input.stop="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change.stop="handleChange"
:aria-label="label"
>
<!-- 前置内容 -->
<span class="el-input__prefix" v-if="context.slots.prefix || prefixIcon">
<slot name="prefix"></slot>
<i class="el-input__icon"
v-if="prefixIcon"
:class="prefixIcon">
</i>
</span>
<!-- 后置内容 -->
<span
class="el-input__suffix"
v-if="getSuffixVisible()">
<span class="el-input__suffix-inner">
<template v-if="!state.showClear || !state.showPwdVisible || !state.isWordLimitVisible">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon"
></i>
</template>
<i v-if="state.showClear"
class="el-input__icon el-icon-circle-close el-input__clear"
@mousedown.prevent
@click="clear"
></i>
<i v-if="state.showPwdVisible"
class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible"
></i>
<span v-if="state.isWordLimitVisible" class="el-input__count">
<span class="el-input__count-inner">
{{ state.textLength }} / {{ state.upperLimit }}
</span>
</span>
</span>
<i class="el-input__icon"
v-if="state.validateState"
:class="['el-input__validateIcon', state.validateIcon]">
</i>
</span>
<!-- 后置元素 -->
<div class="el-input-group__append" v-if="context.slots.append">
<slot name="append"></slot>
</div>
</template>
<textarea
v-else
:tabindex="tabindex"
class="el-textarea__inner"
@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input.stop="handleInput"
ref="textarea"
v-bind="context.attrs"
:disabled="state.inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:style="state.textareaStyle"
@focus="handleFocus"
@blur="handleBlur"
@change.stop="handleChange"
:aria-label="label"
>
</textarea>
<span v-if="state.isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ state.textLength }}/{{ state.upperLimit }}</span>
</div>
</template>
<script>
import '../../../plaginStyle/src/input.scss';
import emitter from '../../../plaginSrc/mixins/emitter.js';
import Migrating from '../../../plaginSrc/mixins/migrating';
import calcTextareaHeight from './calcTextareaHeight';
import merge from '../../../plaginUtils/utils/merge.js';
import {isKorean} from '../../../plaginUtils/utils/shared';
import {ref, reactive, computed, onMounted, watch, inject, getCurrentInstance, onUpdated} from 'vue';
export default {
name: 'ElInput',
componentName: 'ElInput',
inheritAttrs: false,
props: {
modelValue: [String, Number],
size: String,
resize: String,
form: String,
disabled: Boolean,
readonly: Boolean,
type: {
type: String,
default: 'text'
},
autosize: {
type: [Boolean, Object],
default: false
},
autocomplete: {
type: String,
default: 'off'
},
autoComplete: {
type: String,
validator(val) {
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Input]\'auto-complete\'property will be deprecated in next major version. please use \'autocomplete\' instead.');
return true;
}
},
validateEvent: {
type: Boolean,
default: true
},
suffixIcon: String,
prefixIcon: String,
label: String,
clearable: {
type: Boolean,
default: false
},
showPassword: {
type: Boolean,
default: false
},
showWordLimit: {
type: Boolean,
default: false
},
tabindex: String
},
setup(props, context) {
const elForm = inject(elForm, ref(''));
const elFormItem = inject(elFormItem, ref(''));
const currentInstance = getCurrentInstance();
const { dispatch } = emitter(currentInstance);
const textarea = ref(null);
const input = ref(null);
const state = reactive({
textareaCalcStyle: {},
hovering: false,
focused: false,
isComposing: false,
passwordVisible: false,
_elFormItemSize: computed(() => {
return (elFormItem || {})._elFormItemSize;
}),
validateState: computed(() => {
return elFormItem ? elFormItem.validateState : '';
}),
needStatusIcon: computed(() => {
return elForm ? elForm.statusIcon: false;
}),
validateIcon: computed(() => {
return {
validating: 'el-icon-loading',
success: 'el-icon-circle-check',
error: 'el-icon-circle-close'
}[state.validateState];
}),
textareaStyle: computed(() => {
return merge({}, state.textareaCalcStyle, { resize: props.resize});
}),
inputSize: computed(() => {
return props.size || state._elFormItemSize;
}),
inputDisabled: computed(() => {
return props.disabled || (elForm || {}).disabled;
}),
nativeInputValue: computed(() => {
return props.modelValue === null || props.modelValue === undefined ? '' : String(props.modelValue);
}),
showClear: computed(() => {
return props.clearable &&
!state.inputDisabled &&
!props.readonly &&
state.nativeInputValue &&
(state.focused || state.hovering);
}),
showPwdVisible: computed(() => {
return props.showPwdVisible &&
!state.inputDisabled &&
!props.readonly &&
(!!state.nativeInputValue || state.focused);
}),
isWordLimitVisible: computed(() => {
return props.showWordLimit &&
context.attrs.maxlength &&
(props.type === 'text' || props.type === 'textarea') &&
!state.inputDisabled &&
!props.readonly &&
!props.showPassword;
}),
upperLimit: computed(() => {
return context.attrs.maxlength;
}),
textLength: computed(() => {
if (typeof props.modelValue === 'number') {
return String(props.modelValue).length;
}
return (props.modelValue || '').length;
}),
inputExceed: computed(() => {
return state.isWordLimitVisible &&
(state.textLength > state.upperLimit);
})
})
watch(() => props.modelValue, (value, preValue) => {
setTimeout(() => {
resizeTextarea;
}, 2000);
if (props.validateEvent) {
dispatch('ElFormItem', 'el.form.change', [value]);
}
})
watch(() => state.nativeInputValue, (value, preValue) => {
setNativeInputValue();
})
watch(() => props.type, (value, preValue) => {
setTimeout(() => {
setNativeInputValue();
resizeTextarea();
updateIconOffset();
}, 300);
})
function focus() {
getInput().value.focus();
}
function blur() {
getInput().value.blur();
}
function getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
};
}
function handleBlur(event) {
state.focused = false;
context.emit('blur', event);
if (props.validateEvent) {
dispatch('ElFormItem', 'el.form.blur', [props.modelValue]);
}
}
function select() {
getInput().value.select();
}
function resizeTextarea() {
const {autosize, type} = currentInstance.ctx;
if (type !== 'textarea') return;
if (!autosize) {
state.textareaCalcStyle = {
minHeight: calcTextareaHeight(textarea.value || currentInstance.refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
state.textareaCalcStyle = calcTextareaHeight(currentInstance.refs.textarea, minRows, maxRows);
}
function setNativeInputValue() {
const input = getInput();
if (!input) return;
if (!input.value) {
currentInstance.refs.textarea &&
!(currentInstance.refs.textarea.value === state.nativeInputValue) &&
(currentInstance.refs.textarea.value = state.nativeInputValue);
currentInstance.refs.input &&
!(currentInstance.refs.input.value === state.nativeInputValue) &&
(currentInstance.refs.input.value = state.nativeInputValue);
} else {
if (input.value.value === state.nativeInputValue) return;
input.value.value = state.nativeInputValue;
}
}
function handleFocus(event) {
state.focused = true;
context.emit('focus', event);
}
function handleCompositionStart() {
state.isComposing = true;
}
function handleCompositionUpdate(event) {
const text = event.target.value;
const lastCharacter = text[text.length - 1] || '';
state.isComposing = !isKorean(lastCharacter);
}
function handleCompositionEnd(event) {
if (state.isComposing) {
state.isComposing = false;
handleInput(event);
}
}
function handleInput(event) {
if (state.isComposing) return;
if (event.target.value === state.nativeInputValue) return;
context.emit('update:modelValue', event.target.value);
setTimeout(() => {
setNativeInputValue();
}, 300);
}
function handleChange(event) {
// event.stopPropagation();
context.emit('change', event.target.value);
}
function calcIconOffset(place) {
let elList = [].slice.call(currentInstance.vnode.el.querySelectorAll(`.el-input__${place}`) || []);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === currentInstance.vnode.el) {
el = elList[i];
break;
}
}
if (!el) return;
const pendantMap = {
suffix: 'append',
prefix: 'prepend'
};
const pendant = pendantMap[place];
if (context.slots[pendant]) {
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${currentInstance.vnode.el.querySelectorAll(`.el-input-group__${pendant}`).offsetWidth}px)`;
} else {
el.removeAttribute('style');
}
}
function updateIconOffset() {
calcIconOffset('prefix');
calcIconOffset('suffix');
}
function clear() {
context.emit('input', '');
context.emit('change', '')
context.emit('clear');
}
function handlePasswordVisible() {
state.passwordVisible = !state.passwordVisible;
focus();
}
function getInput() {
return input || textarea;
}
function getSuffixVisible() {
return context.slots.suffix ||
props.suffixIcon ||
state.showClear ||
state.showPassword ||
state.isWordLimitVisible ||
(state.validateState && state.needStatusIcon);
}
// 初始执行
// context.on('inputSelect', state.select);
onMounted(() => {
setNativeInputValue();
resizeTextarea();
updateIconOffset();
})
onUpdated(() => {
setTimeout(() => {
updateIconOffset();
}, 300);
})
return {
state,
context,
textarea,
input,
focus,
blur,
getMigratingConfig,
handleBlur,
select,
resizeTextarea,
setNativeInputValue,
handleFocus,
handleCompositionStart,
handleCompositionUpdate,
handleCompositionEnd,
handleInput,
handleChange,
calcIconOffset,
updateIconOffset,
clear,
handlePasswordVisible,
getInput,
getSuffixVisible
}
}
}
</script>
八、element ui 之 form
(一)、源码
form组件:
<template>
<form class="el-form" :class="[
labelPosition ? 'el-form--label-' + labelPosition : '',
{ 'el-form--inline': inline }
]">
<slot></slot>
</form>
</template>
<script>
import objectAssign from 'element-ui/src/utils/merge';
export default {
name: 'ElForm',
componentName: 'ElForm',
provide() {
return {
elForm: this
};
},
props: {
model: Object,
rules: Object,
labelPosition: String,
labelWidth: String,
labelSuffix: {
type: String,
default: ''
},
inline: Boolean,
inlineMessage: Boolean,
statusIcon: Boolean,
showMessage: {
type: Boolean,
default: true
},
size: String,
disabled: Boolean,
validateOnRuleChange: {
type: Boolean,
default: true
},
hideRequiredAsterisk: {
type: Boolean,
default: false
}
},
watch: {
rules() {
// remove then add event listeners on form-item after form rules change
this.fields.forEach(field => {
field.removeValidateEvents();
field.addValidateEvents();
});
if (this.validateOnRuleChange) {
this.validate(() => {});
}
}
},
computed: {
autoLabelWidth() {
if (!this.potentialLabelWidthArr.length) return 0;
const max = Math.max(...this.potentialLabelWidthArr);
return max ? `${max}px` : '';
}
},
data() {
return {
fields: [],
potentialLabelWidthArr: [] // use this array to calculate auto width
};
},
created() {
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
/* istanbul ignore next */
this.$on('el.form.removeField', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
},
methods: {
resetFields() {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for resetFields to work.');
return;
}
this.fields.forEach(field => {
field.resetField();
});
},
clearValidate(props = []) {
const fields = props.length
? (typeof props === 'string'
? this.fields.filter(field => props === field.prop)
: this.fields.filter(field => props.indexOf(field.prop) > -1)
) : this.fields;
fields.forEach(field => {
field.clearValidate();
});
},
validate(callback) {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
// if no callback, return promise
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid) {
valid ? resolve(valid) : reject(valid);
};
});
}
let valid = true;
let count = 0;
// 如果需要验证的fields为空,调用验证时立刻返回callback
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === this.fields.length) {
callback(valid, invalidFields);
}
});
});
if (promise) {
return promise;
}
},
validateField(props, cb) {
props = [].concat(props);
const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
if (!fields.length) {
console.warn('[Element Warn]please pass correct props!');
return;
}
fields.forEach(field => {
field.validate('', cb);
});
},
getLabelWidthIndex(width) {
const index = this.potentialLabelWidthArr.indexOf(width);
// it's impossible
if (index === -1) {
throw new Error('[ElementForm]unpected width ', width);
}
return index;
},
registerLabelWidth(val, oldVal) {
if (val && oldVal) {
const index = this.getLabelWidthIndex(oldVal);
this.potentialLabelWidthArr.splice(index, 1, val);
} else if (val) {
this.potentialLabelWidthArr.push(val);
}
},
deregisterLabelWidth(val) {
const index = this.getLabelWidthIndex(val);
this.potentialLabelWidthArr.splice(index, 1);
}
}
};
</script>
form-item组件
<template>
<div class="el-form-item" :class="[{
'el-form-item--feedback': elForm && elForm.statusIcon,
'is-error': validateState === 'error',
'is-validating': validateState === 'validating',
'is-success': validateState === 'success',
'is-required': isRequired || required,
'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
},
sizeClass ? 'el-form-item--' + sizeClass : ''
]">
<label-wrap
:is-auto-width="labelStyle && labelStyle.width === 'auto'"
:update-all="form.labelWidth === 'auto'">
<label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
<slot name="label">{{label + form.labelSuffix}}</slot>
</label>
</label-wrap>
<div class="el-form-item__content" :style="contentStyle">
<slot></slot>
<transition name="el-zoom-in-top">
<slot
v-if="validateState === 'error' && showMessage && form.showMessage"
name="error"
:error="validateMessage">
<div
class="el-form-item__error"
:class="{
'el-form-item__error--inline': typeof inlineMessage === 'boolean'
? inlineMessage
: (elForm && elForm.inlineMessage || false)
}"
>
{{validateMessage}}
</div>
</slot>
</transition>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator';
import emitter from 'element-ui/src/mixins/emitter';
import objectAssign from 'element-ui/src/utils/merge';
import { noop, getPropByPath } from 'element-ui/src/utils/util';
import LabelWrap from './label-wrap';
export default {
name: 'ElFormItem',
componentName: 'ElFormItem',
mixins: [emitter],
provide() {
return {
elFormItem: this
};
},
inject: ['elForm'],
props: {
label: String,
labelWidth: String,
prop: String,
required: {
type: Boolean,
default: undefined
},
rules: [Object, Array],
error: String,
validateStatus: String,
for: String,
inlineMessage: {
type: [String, Boolean],
default: ''
},
showMessage: {
type: Boolean,
default: true
},
size: String
},
components: {
// use this component to calculate auto width
LabelWrap
},
watch: {
error: {
immediate: true,
handler(value) {
this.validateMessage = value;
this.validateState = value ? 'error' : '';
}
},
validateStatus(value) {
this.validateState = value;
}
},
computed: {
labelFor() {
return this.for || this.prop;
},
labelStyle() {
const ret = {};
if (this.form.labelPosition === 'top') return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
},
contentStyle() {
const ret = {};
const label = this.label;
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
if (!label && !this.labelWidth && this.isNested) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth === 'auto') {
if (this.labelWidth === 'auto') {
ret.marginLeft = this.computedLabelWidth;
} else if (this.form.labelWidth === 'auto') {
ret.marginLeft = this.elForm.autoLabelWidth;
}
} else {
ret.marginLeft = labelWidth;
}
return ret;
},
form() {
let parent = this.$parent;
let parentName = parent.$options.componentName;
while (parentName !== 'ElForm') {
if (parentName === 'ElFormItem') {
this.isNested = true;
}
parent = parent.$parent;
parentName = parent.$options.componentName;
}
return parent;
},
fieldValue() {
const model = this.form.model;
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
return getPropByPath(model, path, true).v;
},
isRequired() {
let rules = this.getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired;
},
_formSize() {
return this.elForm.size;
},
elFormItemSize() {
return this.size || this._formSize;
},
sizeClass() {
return this.elFormItemSize || (this.$ELEMENT || {}).size;
}
},
data() {
return {
validateState: '',
validateMessage: '',
validateDisabled: false,
validator: {},
isNested: false,
computedLabelWidth: ''
};
},
methods: {
validate(trigger, callback = noop) {
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger);
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
resetField() {
this.validateState = '';
this.validateMessage = '';
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
let prop = getPropByPath(model, path, true);
this.validateDisabled = true;
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
// reset validateDisabled after onFieldChange triggered
this.$nextTick(() => {
this.validateDisabled = false;
});
this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
},
getRules() {
let formRules = this.form.rules;
const selfRules = this.rules;
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
getFilteredRule(trigger) {
const rules = this.getRules();
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
},
onFieldBlur() {
this.validate('blur');
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
this.validate('change');
},
updateComputedLabelWidth(width) {
this.computedLabelWidth = width ? `${width}px` : '';
},
addValidateEvents() {
const rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
},
removeValidateEvents() {
this.$off();
}
},
mounted() {
if (this.prop) {
this.dispatch('ElForm', 'el.form.addField', [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
this.addValidateEvents();
}
},
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
};
</script>
label-warp组件
<script>
export default {
props: {
isAutoWidth: Boolean,
updateAll: Boolean
},
inject: ['elForm', 'elFormItem'],
render() {
const slots = this.$slots.default;
if (!slots) return null;
if (this.isAutoWidth) {
const autoLabelWidth = this.elForm.autoLabelWidth;
const style = {};
if (autoLabelWidth && autoLabelWidth !== 'auto') {
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
if (marginLeft) {
style.marginLeft = marginLeft + 'px';
}
}
return (<div class="el-form-item__label-wrap" style={style}>
{ slots }
</div>);
} else {
return slots[0];
}
},
methods: {
getLabelWidth() {
if (this.$el && this.$el.firstElementChild) {
const computedWidth = window.getComputedStyle(this.$el.firstElementChild).width;
return Math.ceil(parseFloat(computedWidth));
} else {
return 0;
}
},
updateLabelWidth(action = 'update') {
if (this.$slots.default && this.isAutoWidth && this.$el.firstElementChild) {
if (action === 'update') {
this.computedWidth = this.getLabelWidth();
} else if (action === 'remove') {
this.elForm.deregisterLabelWidth(this.computedWidth);
}
}
}
},
watch: {
computedWidth(val, oldVal) {
if (this.updateAll) {
this.elForm.registerLabelWidth(val, oldVal);
this.elFormItem.updateComputedLabelWidth(val);
}
}
},
data() {
return {
computedWidth: 0
};
},
mounted() {
this.updateLabelWidth('update');
},
updated() {
this.updateLabelWidth('update');
},
beforeDestroy() {
this.updateLabelWidth('remove');
}
};
</script>
(二)、用vue3重写 form
form组件
<template>
<form class="el-form" :class="[
labelPosition ? 'el-form--label-' + labelPosition : '',
{'el-form--inline': inline}
]">
<slot></slot>
</form>
</template>
<script>
import '../../../plaginStyle/src/form.scss';
import {ref, reactive, computed, provide, watch, getCurrentInstance} from 'vue';
import objectAssign from '../../../plaginUtils/utils/merge.js';
import message from '../../../../../uniApp/components/uni-popup/message';
import { nextTick } from '../../../../../uniApp/wxcomponents/vant/common/utils';
export default {
name: 'ElForm',
componentName: 'ElForm',
props: {
model: Object,
rules: Object,
labelPosition: String,
labelWidth: String,
labelSuffix: {
type: String,
default: ''
},
inline: Boolean,
inlineMessage:Boolean,
statusIcon: Boolean,
showMessage: {
type: Boolean,
default: true
},
size: String,
disabled: Boolean,
validateOnRuleChange: {
type: Boolean,
default: true
},
hideRequiredAsterisk: {
type: Boolean,
default: false
}
},
setup(props, context) {
const state = reactive({
fields: [],
potentialLabelWidthArr: [],
autoLabelWidth: computed(() => {
if (!state.potentialLabelWidthArr.length) return 0;
const max = Math.max(...state.potentialLabelWidthArr);
return max ? `${max}px` : '';
}),
})
const currentInstance = getCurrentInstance();
provide('elForm', currentInstance);
watch(() => props.rules, (rules, preRules) => {
state.fields.forEach(field => {
field.removeValidateEvents();
field.addValidateEvents();
});
if (props.validateOnRuleChange) {
state.validate(() => {});
}
});
// 初始执行
// 监听子组件派发的方法
nextTick(() => {
const children = currentInstance.vnode.el.children;
if (children.length > 0) {
children.forEach((field) => {
state.fields.push(field.__vueParentComponent)
})
}
console.log(state.fields);
})
function resetFields() {
if (!props.model) {
console.warn('[Element Warn][Form]model is required for resetFields to work.');
return;
}
state.fields.forEach(field => {
field.ctx.resetFields();
});
};
function clearValidate(props = []) {
const fields = props.length
? (typeof props === 'string'
? state.fields.filter(field => props === field.prop)
: state.fields.filter(field => props.indexOf(field.prop) > -1)
) : state.fields;
fields.forEach(field => {
field.clearValidate();
})
};
function validate(callback) {
if (!props.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid) {
valid ? resolve(valid) : reject(valid);
};
});
}
let valid = true;
let count = 0;
// 如果需要验证的fields为空,调用验证时立刻返回callback
if (state.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
state.fields.forEach(field => {
field.ctx.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === state.fields.length) {
callback(valid, invalidFields);
}
});
});
if (promise) {
return promise;
}
};
function validateField(props, cb) {
props = [].concat(props);
const fields = state.fields.filter(field => {return props.indexOf(field.ctx.prop) !== -1});
if (!fields.length) {
console.warn('[Element Warn]please pass correct props!');
return;
}
fields.forEach(field => {
field.ctx.validate('', cb);
});
};
function getLabelWidthIndex(width) {
const index = state.potentialLabelWidthArr.indexOf(width);
if (index === -1) {
throw new Error('[ElementForm]unpected width ', width);
}
return index;
};
function registerLabelWidth(val, oldVal) {
if (val && oldVal) {
const index = getLabelWidthIndex(oldVal);
state.potentialLabelWidthArr.splice(index, 1, val);
} else if (val) {
state.potentialLabelWidthArr.push(val);
}
};
function deregisterLabelWidth(val) {
const index = getLabelWidthIndex(val);
state.potentialLabelWidthArr.splice(index, 1);
}
return {
state,
currentInstance,
context,
resetFields,
clearValidate,
validate,
validateField,
getLabelWidthIndex,
registerLabelWidth,
deregisterLabelWidth
}
}
}
</script>
form-item组件
<template>
<div class="el-form-item" :class="[{
'el-form-item--feedback': elForm && elForm.ctx.statusIcon,
'is-error': state.validateState === 'error',
'is-validating': state.validateState === 'validating',
'is-success': state.validateState === 'success',
'is-required': state.isRequired || required,
'is-no-asterisk': elForm && elForm.ctx.hideRequiredAsterisk
},
state.sizeClass ? 'el-form-item--' + state.sizeClass : ''
]">
<label-wrap
:is-auto-width="state.labelStyle && state.labelStyle.width === 'auto'"
:update-all="state.form.ctx.labelWidth === 'auto'">
<label :for="state.labelFor" class="el-form-item__label" :style="state.labelStyle" v-if="label || context.slots.label">
<slot name="label">{{label + state.form.ctx.labelSuffix}}</slot>
</label>
</label-wrap>
<div class="el-form-item__content" :style="state.contentStyle">
<slot></slot>
<transition name="el-zoom-in-top">
<slot
v-if="state.validateState === 'error' && showMessage && state.form.ctx.showMessage"
name="error"
:error="state.validateMessage">
<div
class="el-form-item__error"
:class="{
'el-form-item__error--inline': typeof inlineMessage === 'boolean'
? inlineMessage
: (elForm && elForm.ctx.inlineMessage || false)
}">
{{state.validateMessage}}
</div>
</slot>
</transition>
</div>
</div>
</template>
<script>
import '../../../plaginStyle/src/form-item.scss';
import AsyncValidator from 'async-validator';
import emitter from '../../../plaginSrc/mixins/emitter';
import objectAssign from '../../../plaginUtils/utils/merge';
import fun from '../../../plaginUtils/utils/util';
import LabelWrap from './label-wrap';
import {ref, reactive, provide, inject, getCurrentInstance, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue';
export default {
name: 'ElFormItem',
componentName: 'ElFormItem',
props: {
label: String,
labelWidth: String,
prop: String,
required: {
type: Boolean,
default: undefined
},
rules: [Object, Array],
error: String,
validateStatus: String,
for: String,
inlineMessage: {
type: [String, Boolean],
default: ''
},
showMessage: {
type: Boolean,
default: true
},
size: String
},
components: {
LabelWrap
},
setup(props, context) {
const currentInstance = getCurrentInstance();
provide('elFormItem', currentInstance);
const elForm = inject('elForm', null);
const {dispatch} = emitter(currentInstance);
const {getPropByPath} = fun;
const state = reactive({
validateState: '',
validateMessage: '',
validateDisabled: false,
validator: {},
isNested: false,
computedLabelWidth: '',
labelFor: computed(() => {
return props.for || props.prop;
}),
labelStyle: computed(() => {
const ret = {};
if (state.form.ctx.labelPosition === 'top') return ret;
const labelWidth = props.labelWidth || state.form.ctx.labelWidth;
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
}),
contentStyle: computed(() => {
const ret = {};
const label = props.label;
if (state.form.ctx.labelPosition === 'top' || state.form.ctx.inline) return ret;
if (!label && !props.labelWidth && state.isNested) return ret;
const labelWidth = props.labelWidth || state.form.ctx.labelWidth;
if (labelWidth === 'auto') {
if (props.labelWidth === 'auto') {
ret.marginLeft = state.computedLabelWidth;
} else if (state.form.ctx.labelWidth === 'auto') {
ret.marginLeft = elForm.ctx.autoLabelWidth;
}
} else {
ret.marginLeft = labelWidth;
}
return ret;
}),
form: computed(() => {
let parent = currentInstance.parent;
let parentName = parent.type.componentName;
while (parentName !== 'ElForm') {
if (parentName === 'ElFormItem') {
state.isNested = true;
}
parent = parent.parent;
parentName = parent.type.componentName;
}
return parent;
}),
fieldValue: computed(() => {
const model = state.form.ctx.model;
if (!model || !props.prop) { return; }
let path = props.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
return getPropByPath(model, path, true).v;
}),
isRequired: computed(() => {
let rules = getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
})
}
return isRequired;
}),
_formSize: computed(() => {
return elForm.ctx.size;
}),
elFormItemSize: computed(() => {
return props.size || state._formSize;
}),
sizeClass: computed(() => {
return state.elFormItemSize
})
});
watch(() => props.error, (value, prevalue) => {
state.validateMessage = value;
state.validateState = value ? 'error' : '';
});
watch(() => state.validateStatus, (value) => {
state.validateState = value;
});
onMounted(() => {
if (props.prop) {
dispatch('ElForm', 'el.form.addField', [currentInstance]);
let initialValue = state.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
// Object.assign({}, currentInstance.ctx, {
// initialValue: {
// value: initialValue
// }
// })
Object.defineProperty(currentInstance.ctx, 'initialValue', {
value: initialValue
})
addValidateEvents();
}
});
onBeforeUnmount(() => {
dispatch('ElForm', 'el.form.removeField', [currentInstance]);
});
function validate(trigger, callback = noop) {
state.validateDisabled = false;
const rules = getFilteredRule(trigger);
// rules没有不需校验,直接返回true
if ((!rules || rules.length === 0) && props.required === undefined ) {
callback();
return true;
}
state.validateState = 'validating';
// 如果存在rule,将rules的每一项中的trigger去掉 并赋值
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
})
}
descriptor[props.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
model[props.prop] = state.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
state.validateState = !errors ? 'success' : 'error';
state.validateMessage = errors ? errors[0].message : '';
callback(state.validateMessage, invalidFields);
elForm && elForm.emit('validate', props.prop, !errors, state.validateMessage || null);
});
};
function clearValidate() {
state.validateState = '';
state.validateMessage = '';
state.validateDisabled = false;
};
function resetFields() {
state.validateState = '';
state.validateMessage = '';
let model = state.form.model;
let value = state.fieldValue;
let path = props.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
let prop = getPropBypath(model, path, true);
this.validateDisabled = true;
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(state.initialValue);
} else {
prop.o[prop.k] = state.initialValue;
}
const changeValidateDisabled = async () => {
state.validateDisabled = false;
await nextTick();
}
state.broadcast('ElTimeSelect', 'fieldReset', state.initialValue);
};
function getRules() {
let formRules = state.form.ctx.rules; // form的校验规则
const selfRules = props.rules; // form-item自身的校验规则
const requiredRule = props.required !== undefined ? { required: !!props.required } : []; // 直接required属性
const prop = getPropByPath(formRules, props.prop || '');
formRules = formRules ? (prop.o[props.prop || ''] || prop.v) : [];
return [].concat(selfRules || formRules || []).concat(requiredRule);
};
function getFilteredRule(trigger) {
const rules = getRules(); // 获取所有需要检测的项
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => {
return objectAssign({}, rule)
}); // 对需要检测的项,返回
};
function onFieldBlur() {
validate('blur');
};
function onFieldChange() {
if (state.validateDisabled) {
state.validateDisabled = false;
return;
}
validate('change');
};
function updateComputedLabelWidth(width) {
state.computedLabelWidth = width ? `${width}px`: '';
};
function addValidateEvents() {
const rules = getRules();
if (rules.length || props.required !== undefined) {
// 向form传值
}
};
function removeValidateEvents() {
currentInstance.off();
}
return {
state,
context,
elForm,
currentInstance,
validate,
clearValidate,
resetFields,
getRules,
getFilteredRule,
onFieldBlur,
onFieldChange,
updateComputedLabelWidth,
addValidateEvents,
removeValidateEvents
}
}
}
</script>
label-warp组件
<template>
<div v-if="context.slots.default">
<!-- v-if="isAutoWidth" -->
<div
class="el-form-item__label-wrap"
:style="state.style">
<slot></slot>
</div>
<!-- <template>
{{context.slots[0]}}
</template> -->
</div>
</template>
<script>
import {ref, reactive, inject, onMounted, onUpdated, onBeforeUnmount, watch, getCurrentInstance, render, h} from 'vue';
export default {
props: {
isAutoWidth: Boolean,
updateAll: Boolean
},
setup(props, context) {
const elForm = inject('elForm', null);
const elFormItem = inject('elFormItem', null);
const currentInstance = getCurrentInstance();
const state = reactive({
computedWidth: 0,
style: {}
})
watch(() => state.computedWidth, (val, oldVal) => {
if (props.updateAll) {
elForm.ctx.registerLabelWidth(val, oldVal);
elFormItem.ctx.updateComputedLabelWidth(val);
}
})
onMounted(() => {
updateLabelWidth('update');
});
onUpdated(() => {
updateLabelWidth('update');
})
onBeforeUnmount(() => {
updateLabelWidth('remove');
})
function getLabelWidth() {
if (currentInstance.vnode.el && currentInstance.vnode.el.firstElementChild) {
const computedWidth = window.getComputedStyle(currentInstance.vnode.el.firstElementChild).width;
return Math.ceil(parseFloat(computedWidth));
} else {
return 0;
}
}
function updateLabelWidth(action = 'update') {
if (currentInstance.slots.default && props.isAutoWidth && currentInstance.vnode.el.firstElementChild) {
if (action === 'update') {
state.computedWidth = getLabelWidth();
} else if (action === 'remove') {
elForm.deregisterLabelWidth(state.computedWidth);
}
}
}
(() => {
const slots = context.slots.default;
if (!slots) return null;
if (props.isAutoWidth) {
const autoLabelWidth = elForm.autoLabelWidth;
const style = {};
if (autoLabelWidth && autoLabelWidth !== 'auto') {
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
if (marginLeft) {
style.marginLeft = marginLeft + 'px';
}
}
state.style = style;
} else {
return slots[0]
}
})
return {
state,
context,
getLabelWidth,
updateLabelWidth
}
}
}
</script>
九、vue3中引入插件的方式,及其他差异点
- vue3使用插件方式,使用use方法
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './assets/js/flexible.debug'
import { Button, Skeleton } from 'vant';
import ELButton from './plagins/button/index.js';
import ElRow from './plagins/row/index.js';
import ElCol from './plagins/col/index.js';
import ElContainer from './plagins/container/index.js';
import ElHeader from './plagins/header/index.js';
import ElAside from './plagins/aside/index.js';
import ElLink from './plagins/link/index.js';
import ElRadio from './plagins/radio/index.js';
import ElRadioGroup from './plagins/radio-group/index.js';
import ElRadioButton from './plagins/radio-button/index.js';
import ElCheckbox from './plagins/checkbox/index.js';
import ElCheckboxGroup from './plagins/checkbox-group/index.js';
import ElCheckboxButton from './plagins/checkbox-button/index.js';
import ElInput from './plagins/input/index.js';
import ElForm from './plagins/form/index.js';
import ElFormItem from './plagins/form-item/index.js';
createApp(App).use(store).use(router)
.use(Button).use(Skeleton)
.use(ELButton)
.use(ElRow)
.use(ElCol)
.use(ElContainer)
.use(ElHeader)
.use(ElAside)
.use(ElLink)
.use(ElRadio)
.use(ElRadioGroup)
.use(ElRadioButton)
.use(ElCheckbox)
.use(ElCheckboxGroup)
.use(ElCheckboxButton)
.use(ElInput)
.use(ElForm)
.use(ElFormItem)
.mount('#app')
- 具名插槽使用方式
- 组件定义
在setup里面直接用ref()定义,名称为组件名,初始值为空即可
总结
写到后面就不想再写文字了,主要目的是记录一下代码,顺带写一下差异,基本上的差异都列举出来了,以后复习应该看得懂