Override the Button component
background
Some of you may ask why we should rewrite components.
The implementation logic for element3 components is now forcefully rewritten from the Options API to the Composition API
Code is poorly organized, unreadable, unmaintainable, and unextensible
And you might ask why don’t you refactor it from the original logic?
To be honest, the original logic is so messy that it can even affect your thinking
So let’s be bold and rewrite
This article is a detailed document of how and how to refactor Button components
This is mainly for students who want to contribute to the source code to rewrite the component ideas
This article is very dry, probably full of code. Please read with caution
process
Rewriting a component can be broken down into the following points
- Confirm the demand
- Tasking
- Tdd
- snapshot
Let’s take a look at them in turn
demand
So before we rewrite this let’s define what we have to rewrite to satisfy our needs, right
First, the external interface cannot be modified, such as:
- props
- emits
- slots
These are all external interfaces, and they should be consistent with the original logic
Then our logic is to implement it using the Composition API
Last but not least, you need to keep unit test coverage above 90 percent
Ok, so that’s what we need for component rewriting
Tasking
In line with the idea of starting with the end, we need to determine what functions a Button has, so we can list them one by one
In fact, if we look at the element website documentation about buttons, we know exactly what functions buttons have
Feature list
- The size of a Button can be set based on the size property
- The type of a Button can be set based on the type attribute
- Button style is inconsistent for different types
- You can set whether a Button is a plain Button based on the plain property
- Plain button is also a style change
- You can set whether a Button is a rounded Button based on the round property
- It’s also a change in style
- You can set whether a Button is a circular Button based on the circle property
- It’s a change of style
- Using the Loading property, you can set whether a Button is in loading state
- If loading is set, a “loading” icon will be displayed
- You can set whether a Button is disabled based on the disabled property
- Style change to show a disabled icon
- You can’t click
- The icon displayed on a Button can be set based on the icon property
- Using the AutoFocus property, you can set whether a Button is focused by default
- The Button native type property can be set based on the native type property
In addition to these surface function points, there are actually some more detailed function points, such as:
- In loading state, ICONS set by icon cannot be displayed
- This means that a component can only have one icon displayed
- Not loading, not setting the icon
- This means that a component can only have one icon displayed
- In loading state, the component cannot be clicked
- You can have three points to control the size of a Button
- Own props
- When the parent FormItem, you get the Size of the Item
- You can set size through global configuration
- You can have two points to control the Disabled of a Button
- Own props
- If the parent is Form, form. disabled can also be controlled
- If either of the above points is true, the Button will not be displayed
- You can define the contents of components in slot mode
I’ve listed all of the Button functions before, but rewriting a component is the most important part of the process
My own habit is to list all my tasks
Then, when a task is completed, tick one
It’s like playing a game and doing tasks. Every time you check a box, you get a +1 experience
Of course, I call this “seeing progress.”
This will give you an idea of how close you are to completing the feature
TDD
Some of you may ask what is TDD? Here I will not science, interested students can baidu to learn
Briefly, TDD is a programming approach
- Write a failed test first
- Then write only the logic that makes the failed test pass
- refactoring
So the question is, what are we writing unit tests for? In fact, the points we want to test have been enumerated in the Tasking step
This chapter actually involves a lot of small refactoring steps, which would be a waste of time to write them all out, so I adopted the form of post code to improve efficiency
You can define the contents of components in slot mode
Look for the simplest thing to do first. This is the easiest
Look for the soft ones first
test
import Button from '.. /src/Button.vue'
import { mount } from '@vue/test-utils'
describe('Button.vue'.() = > {
it('should show content'.() = > {
const content = 'foo'
const wrapper = mount(Button, {
slots: {
default: content
}
})
expect(wrapper.text()).toContain(content)
})
})
Copy the code
Code implementation
<template>
<button>
<slot></slot>
</button>
</template>
<script>
export default {
setup() {
return {}
}
}
</script>
Copy the code
The size of a Button can be set based on the size property
test
describe('set button size'.() = > {
it.only('by props.size'.() = > {
const size = 'small'
const wrapper = mount(Button, {
props: {
size
}
})
expect(wrapper.classes()).toContain(`el-button--${size}`)})})Copy the code
ToContain is an assertion that can help print out classes currently owned by the wrapper if the test fails, making it easier to debug
Code implementation
<template> <button class="el-button" :class="[ buttonSize ? `el-button--${size}` : '' ]" > <slot></slot> </button> </template> <script> import { toRefs } from 'vue' export default { props: { size: { type: String, validator(val) { if(val === "") return true return ['medium', 'small', 'mini'].indexOf(val) ! == -1 } }, } } </script>Copy the code
Here we implement the props size check
Based on elFormItem elFormItemSize to set the size of the Button
test
it('by elFormItem.elFormItemSize'.() = > {
const size = 'small'
const wrapper = mount(Button, {
global: {
provide: {
elFormItem: reactive({
elFormItemSize: size
})
}
}
})
expect(wrapper.classes(`el-button--${size}`)).toBeTruthy()
})
Copy the code
Code implementation
<template> <button class="el-button" :class="[ buttonSize ? `el-button--${buttonSize}` : '', ]" > <slot></slot> </button> </template> <script> import { toRefs, inject, computed } from 'vue' export default { props: [ size: { type: String, validator(val) { if (val === '') return true return ['medium', 'small', 'mini'].indexOf(val) !== -1 } }, ], setup(props) { const { size } = toRefs(props) const buttonSize = useButtonSize(size) return { buttonSize } } } const useButtonSize = (size) => { return computed(() => { const elFormItem = inject('elFormItem', {}) return size? .value || elFormItem.elFormItemSize }) } </script>Copy the code
Because of the test to do the guarantee, refactoring is also very confident
Set the size of the Button based on the global configuration size
test
it('by global config '.() = > {
const size = 'small'
const wrapper = mount(Button, {
global: {
config: {
globalProperties: {
$ELEMENT: {
size
}
}
}
}
})
expect(wrapper.classes()).toContain(`el-button--${size}`)})Copy the code
Code implementation
const useButtonSize = (size) = > {
return computed(() = > {
const elFormItem = inject('elFormItem', {})
return( size? .value || elFormItem.elFormItemSize || getCurrentInstance().ctx.$ELEMENT? .size ) }) }Copy the code
We have succeeded in the task of size
The type of a Button can be set based on the type attribute
test
it('set button type by prop type '.() = > {
const type = 'success'
const wrapper = mount(Button, {
props: {
type
}
})
expect(wrapper.classes()).toContain(`el-button--${size}`)})Copy the code
Code implementation
<template> <button class="el-button" :class="[ buttonSize ? `el-button--${buttonSize}` : '', type ? `el-button--${type}` : '' ]" > <slot></slot> </button> </template> <script> export default { props: { size: { type: String, validator(val) { if (val === '') return true return ['medium', 'small', 'mini'].indexOf(val) ! == -1 } }, type: { type: String, validator(val) { return ( ['primary', 'success', 'warning', 'danger', 'info', 'text'].indexOf( val ) ! == -1 ) } } } </script>Copy the code
Class is used to control the style of display type
You can set whether a Button is a plain Button based on the plain property
test
it('set button plain by prop type'.() = > {
const wrapper = mount(Button, {
props: {
plain: true
}
})
expect(wrapper.classes()).toContain(`is-plain`)})Copy the code
Code implementation
<template>
<button
class="el-button"
:class="[
buttonSize ? `el-button--${buttonSize}` : '',
type ? `el-button--${type}` : '',
{
'is-plain': plain
}
]"
>
<slot></slot>
</button>
</template>
<script>
...
props:{
plain: Boolean
}
...
</script>
Copy the code
You can set whether a Button is a rounded Button based on the round property
test
it('set button round by prop type'.() = > {
const wrapper = mount(Button, {
props: {
round: true
}
})
expect(wrapper.classes()).toContain(`is-round`)})Copy the code
Code implementation
<template> <button class="el-button" :class="[ buttonSize ? `el-button--${buttonSize}` : '', type ? `el-button--${type}` : '', { 'is-plain': plain, 'is-round': Round}] "> < slot > < / slot > < / button > < / template > < script >... Props: {round: Boolean}... </script>Copy the code
Just add a class
You can set whether a Button is a circular Button based on the circle property
test
it('set button circle by prop type'.() = > {
const wrapper = mount(Button, {
props: {
circle: true
}
})
expect(wrapper.classes()).toContain(`is-circle`)})Copy the code
Code implementation
<template> ... { 'is-plain': plain, 'is-round': round, 'is-circle': circle } ... > < / template > < script >... Props :{circle: Boolean}...... </script>Copy the code
Set Loading to make the button appear loaded
If the loading state is in, the button should not be clicked and the Loading icon should be displayed
test
it('set button loading by prop loading'.async() = > {const wrapper = mount(Button, {
props: {
loading: true
}
})
expect(wrapper.classes()).toContain(`is-loading`)
expect(wrapper.attributes()).toHaveProperty('disabled')})Copy the code
You just need to verify that the button has a disabled property
Code implementation
<template>
...
:disabled="loading"
:class="[
{
'is-plain': plain,
'is-round': round,
'is-circle': circle,
'is-loading': loading
}
]
<i class="el-icon-loading" v-if="loading"></i>
<slot></slot>
...
>
</template>
<script>
export default {
props:{
loading: Boolean
}
}
}
</script>
Copy the code
You can set whether a Button is disabled based on the disabled property
test
describe('set button disabled'.() = > {
it('by props.disabled'.() = > {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.classes()).toContain(`is-disabled`)
expect(wrapper.attributes()).toHaveProperty('disabled')})})Copy the code
Because disabled involves two function points, one through props and one through the parent component Form, we use describe to organize the tests
This test is slightly different from the previous one. Not only do we need to verify that there is an IS-Disabled class name, we also need to set the component to disabled to make it invalid
Code implementation
<template> <button class="el-button" :disabled="disabled || loading" :class="[ buttonSize ? `el-button--${buttonSize}` : '', type ? `el-button--${type}` : '', { 'is-disabled': disabled } ]" ></template> <script> props:{ disabled: Boolean } </script>Copy the code
If the parent component is not From and the value of Disabled From is true, the current component is also affected
test
it('by elForm.disable'.() = > {
const wrapper = mount(Button, {
global: {
provide: {
elForm: reactive({
disabled: true
})
}
}
})
expect(wrapper.classes()).toContain(`is-disabled`)
expect(wrapper.attributes()).toHaveProperty('disabled')})Copy the code
Code implementation
<template> <button class="el-button" :disabled="buttonDisabled || loading" :class="[ buttonSize ? `el-button--${buttonSize}` : '', type ? `el-button--${type}` : '', { 'is-plain': plain, 'is-round': round, 'is-circle': circle, 'is-loading': loading, 'is-disabled': buttonDisabled } ]" > <slot></slot> </button> </template> <script> setup(props){ const { size, disabled } = toRefs(props) const buttonDisabled = useButtonDisabled(disabled) return { ... buttonDisabled } } const useButtonDisabled = (disabled) => { return computed(() => { const elForm = inject('elForm', {}) return disabled? .value || elForm.disabled }) } </script>Copy the code
The icon displayed on a Button can be set based on the icon property
test
it('set button icon by props.icon'.() = > {
const wrapper = mount(Button, {
props: {
icon: 'el-icon-edit'
}
})
expect(wrapper.find('.el-icon-edit').exists()).toBe(true)})Copy the code
Detecting the presence of an element requires a combination of find + exists
Code implementation
< the template >... + <i :class="icon" v-if="icon"></i> </button> </template> <script> props:{ icon:String } </script>Copy the code
Continue, we have a logic, if loading displays, then the icon can not be displayed
In loading state, ICONS set by icon cannot be displayed
test
it("don't show icon when loading eq true".() = > {
const wrapper = mount(Button, {
props: {
icon: 'el-icon-edit'.loading: true
}
})
expect(wrapper.find('.el-icon-edit').exists()).toBe(false)
expect(wrapper.find('.el-icon-loading').exists()).toBe(true)})Copy the code
Code implementation
< the template >... < I class = "el - icon - loading" v - if = "loading" > < / I > < I: class = "icon" v - else - if = "icon" > < / I >... </template>Copy the code
It’s easy to implement because loading and icon can only be one, so we use v-else-if
Using the AutoFocus property, you can set whether a Button is focused by default
This doesn’t need to be implemented, it will automatically be added to the inner button when you set autoFocus on the outside
<Button autofocus></Button>
Copy the code
The Button native type property can be set based on the native type property
test
it('set native-type by props.native-type'.() = > {
const nativeType = 'reset'
const wrapper = mount(Button, {
props: {
nativeType
}
})
expect(wrapper.attributes('type')).toBe(nativeType)
})
Copy the code
Code implementation
<template>
<button
:type="nativeType"
>
</button>
</template>
<script>
props:{
nativeType:String
}
</script>
Copy the code
refactoring
Before the refactoring
<template> <button class="el-button" :type="nativeType" :disabled="buttonDisabled || loading" :class="[ buttonSize ? `el-button--${buttonSize}` : '', type ? `el-button--${type}` : '', { 'is-plain': plain, 'is-round': round, 'is-circle': circle, 'is-loading': loading, 'is-disabled': buttonDisabled } ]" > <i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-else-if="icon"></i> <slot></slot> </button> </template> <script> import { toRefs, inject, computed, getCurrentInstance } from 'vue' export default { props: { size: { type: String, validator(val) { if (val === '') return true return ['medium', 'samll', 'mini'].indexOf(val) ! == -1 } }, type: { type: String, validator(val) { return ( ['primary', 'success', 'warning', 'danger', 'info', 'text'].indexOf( val ) ! == -1 ) } }, plain: Boolean, round: Boolean, circle: Boolean, loading: Boolean, disabled: Boolean, icon: String, nativeType: String }, setup(props) { const { size, disabled } = toRefs(props) const buttonSize = useButtonSize(size) const buttonDisabled = useButtonDisabled(disabled) return { buttonSize, buttonDisabled } } } const useButtonDisabled = (disabled) => { return computed(() => { const elForm = inject('elForm', {}) return disabled? .value || elForm.disabled }) } const useButtonSize = (size) => { return computed(() => { const elFormItem = inject('elFormItem', {}) return ( size? .value || elFormItem.elFormItemSize || getCurrentInstance().ctx.$ELEMENT? .size ) }) } </script>Copy the code
I don’t really like the idea of classes being handled in templates, so I’m going to refactor this logical point
Thanks to unit testing, I was able to refactor with confidence
After the reconstruction
<template>
<button
class="el-button"
:class="classes"
:type="nativeType"
:disabled="buttonDisabled || loading"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-else-if="icon"></i>
<slot></slot>
</button>
</template>
<script>
import { toRefs, inject, computed, getCurrentInstance } from 'vue'
export default {
name: 'ElButton'.props: {
size: {
type: String.validator(val) {
if (val === ' ') return true
return ['large'.'medium'.'small'.'mini'].indexOf(val) ! = = -1}},type: {
type: String.validator(val) {
return(['primary'.'success'.'warning'.'danger'.'info'.'text'].indexOf( val ) ! = = -1)}},nativeType: {
type: String.default: 'button'
},
plain: Boolean.round: Boolean.circle: Boolean.loading: Boolean.disabled: Boolean.icon: String
},
setup(props) {
const { size, disabled } = toRefs(props)
const buttonSize = useButtonSize(size)
const buttonDisabled = useButtonDisabled(disabled)
const classes = useClasses({
props,
size: buttonSize,
disabled: buttonDisabled
})
return {
buttonDisabled,
classes
}
}
}
const useClasses = ({ props, size, disabled }) = > {
return computed(() = > {
return [
size.value ? `el-button--${size.value}` : ' ',
props.type ? `el-button--${props.type}` : ' ',
{
'is-plain': props.plain,
'is-round': props.round,
'is-circle': props.circle,
'is-loading': props.loading,
'is-disabled': disabled.value
}
]
})
}
const useButtonDisabled = (disabled) = > {
return computed(() = > {
const elForm = inject('elForm', {})
returndisabled? .value || elForm.disabled }) }const useButtonSize = (size) = > {
return computed(() = > {
const elFormItem = inject('elFormItem', {})
return( size? .value || elFormItem.elFormItemSize || getCurrentInstance().ctx.$ELEMENT? .size ) }) }</script>
Copy the code
So far, all of our tasks have been completed. I don’t know if you feel it, but we are only focusing on one small feature at a time, which is very simple to implement
Now that the component logic is complete, it’s time to look at the component styles
Increase the snapshot
Before adding snapshot, we need to look at the component styles manually, since we didn’t look at the UI during the TDD process
The Snapshot test
it('snapshot'.() = > {
const wrapper = mount(Button)
expect(wrapper.element).toMatchSnapshot()
})
Copy the code
The snapshot test is simple. After writing a few lines of code, JEST will help us generate a snapshot of the current component
// button/tests/_snapshots__/Button.spec.js.snap // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Button.vue snapshot 1`] = ` <button class="el-button" type="button" > <! --v-if--> </button> `;Copy the code
## Test coverage
Finally, based on our needs, we want to achieve 90% test coverage
Let’s see what our coverage is now
Run the following command
yarn test packages/button/tests/Button.spec.js --coverage
Copy the code
You can see the following results
PASS packages/button/tests/button. The spec. Js button. Vue ✓ snapshot (20Ms) ✓ Should show content (10✓ ms) ✓ set button type by prop type (2Ms) ✓ set button plain by prop type (2Ms) ✓ set button round by prop type (2Ms) ✓ set button circle by prop type (2Ms) ✓ set button loading by prop loading (2Ms) ✓ set button loading by prop loading (2Ms) ✓ set native type by props. Native type (ms) ✓2Ms) set button size predictably by props. Size (3Ms) ✓ by elFormItem. ElFormItemSize (1Ms) ✓ byglobal config (2Ms) set button disabled ✓ by props.2Ms) ✓ by elform.disable (1Ms) Set button icon ✓ by props.6Ms) ✓ don't show icon when loading eq true (2 ms) -----------------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -----------------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | src | 100 | 100 | 100 | 100 | Button.vue | 100 | 100 | 100 | 100 | tests | 100 | 100 | 100 | 100 | Button.spec.js | 100 | 100 | 100 | 100 | -----------------|---------|----------|---------|---------|------------------- Test Suites: Values: 1 passed, 1 Total Tests: 16 passed, 16 total Snapshots: 1 passed, 1 Total Time: 3.359sCopy the code
The test coverage reached 100 percent
Because we are developing with TDD, achieving 100 percent test coverage is a common practice
conclusion
That’s all for rewriting the Button component, a little summary
We need to determine the functionality of the component first
Then implement it bit by bit based on TDD
We end up with a component with 100% test coverage
Even in complex components, functions are implemented by small functions. In the process of TDD, we actually reduce the mental burden, so that we only care about the implementation of a small function, and because of the guarantee of testing, we can refactor at any time
All subsequent components of Element3 will be overridden in the same way.
The quality of the code is ensured to the greatest extent possible, and of course this is also for the subsequent extension of new features
Subsequent articles will simplify the TDD steps because they are too much trouble!!
- This is the open source project Element3 of our Huaguoshan team
- A front end component library that supports VUE3