This is my first article on getting started
One, foreword
There are a thousand Hamlets in a thousand people’s eyes!
Components can be encapsulated in many ways, and what works for you and your team is best practice.
Some time ago, I received a request to have an interface showing the step bar. I thought it was not easy to have Vant UI (///▽///). I wanted to finish it and then I could paddle. But look at Vant Steps, there are too few properties to do. Have no way can oneself develop a, say stem stem 😊 I like work most! I love working!! I love to work!! .
Second, the target
Our goals are:
- An underlying component that is single, reusable, and not coupled to any business function.
- It has the ability of generality and personalized configuration.
- Nodes can be flexibly changed, icon can be configured, direction can be configured and other functions…
- After defining our goals, we started to design features around our goals.
Three, function planning definition: prop, slot, event
From little acorns, even complex components are made up of these three parts
Through prop, slot and event, we can quickly understand all the functions of a component
It is important to design these three parts before you develop your component, so that both users and others who read your component can read them in depth and quickly.
The development of a component consists of three steps:
- Component abstraction (general purpose, flexible, high cohesion, low coupling)
- The more abstract a component definition is, the more flexible and extensible it is. But it also brings the disadvantage of “incomprehension”.
- It is recommended to use the “intuitive first” principle, the logical intuitive first principle,
- Interface implementation (HTML, CSS, JavaScript implementation)
- Establish association (component communication, component function)
1. My thoughts on component abstraction
1. Dimensions of component splitting: properties and methods
- Example: the person:
- Attributes: height, weight, age
- Method: people: Method: eat, sleep, write bugs
- Remove the changing and the invariable, and the moving and the static, to ensure that the changing part is flexible and the invariable part is stable
- When I pull out the “change”, it provides infinite possibilities for subsequent expansion (provide possibilities for changing requirements 👀)
Component Development Principles:
- Single function principle
- By separating the different responsibilities, each one can be flexibly reused
- A component is focused on doing one thing and doing it well.
2. Step bar component props
- Abstract of the step bar component
- The parent component:
- Properties: currently selected node, container direction, container background color
- Child components:
- Properties: current step status, active, inactive, complete status icon/color, title
- Method: The click event for the component
- Extensions: Icon slot, title slot
- The parent component:
I want the ability of the parent container:
- Can control the progress bar step, control the direction of the step bar
I want the ability of sub-components:
- Configurable title, color, icon, self – controlled status
Props Design principles:
-
Component properties should have default values. Using the default values allows us to pass fewer parameters.
-
Component properties should avoid using complex data structures such as objects. Simple property values are easy to understand and maintain
-
Components should be robust enough to allow for boundary exceptions, type validation of attributes, not defaults (never trust user input)
Ps: When designing props, I had no idea whether to configure icon and color in the parent container or in the child container. Then I had a second thought: is it appropriate for you 🧑🏻🎤 to give clothes to your girlfriend’s father 👨 and ask your girlfriend’s father to help your girlfriend change clothes 👗? 👀 mom said: their own things to do ~
File structure
Steps Parent container props
props: {
active: { // The current active step
type: Number.default: 0
},
direction: { // Container direction
type: String.validator(value) { // Use the validator property to verify that the parameter input is valid.
return ['horizontal'.'vertical'].find(e= > e === value)
},
default: 'horizontal'
},
background: { // The container background color
type: String.default: '#ffffff'}}Copy the code
Stpe child container props
props: {
title: { / / title
type: String.default: ' '
},
status: { // Use steps to set the status of the current step
type: String.default: ' '
},
inactiveColor: { // Inactive state color
type: String.default: '# 999999'
},
activeColor: { // Active state color
type: String.default: '# 333333'
},
finishColor: { // Finished state color
type: String.default: '# 999999'
},
inactiveIcon: { // Inactive status icon
type: String.default: require('./icons/inactive.png')},activeIcon: { // Activate the status icon
type: String.default: require('./icons/active.png')},finishIcon: { // Finished status icon
type: String.default: require('./icons/finish.png')}}Copy the code
3. Step bar component slot
Slot here design is relatively simple, PS: nothing good to write
Steps Parent container Slot Default slot
<template>
<div>
<slot></slot>
</div>
</template>
Copy the code
Stpe Indicates the slot of the child container
<template>
<div>
<slot name="icon"><slot>
<slot name="title"></slot>
</div>
</template>
Copy the code
Ps: How do I get the number of component slots?
This.$slots is used to access the contents of slots. The default array contains all the default slots. A named slot is rendered as an object.
console.log(this.$slots)
/** * default: Array(4) * 0: VNode {tag: "vue-component-209-custom-step", data: {... }, children: undefined, text: undefined, elm: div.custom-step__center.active,... } * 1: VNode {tag: "vue-component-209-custom-step", data: {... }, children: undefined, text: undefined, elm: div.custom-step__center. Inactive,... } * 2: VNode {tag: "vue-component-209-custom-step", data: {... }, children: undefined, text: undefined, elm: div.custom-step__center. Inactive,... } * 3: VNode {tag: "vue-component-209-custom-step", data: {... }, children: undefined, text: undefined, elm: div.custom-step__center. Inactive,... } * length: 4 */
Copy the code
Step bar Components provide and Inject
There are many ways for components to communicate with each other, but they all have some drawbacks
How components communicate with each other:
- props/$emit
- $ref/ $parent/ $children
- $on/ $emit
- vuex
- $attrs/ $listeners
- provide/inject
- boradcast/dispatch
- observable
Due to the reason of space, I will not introduce ~ ~
Provide/inject, $parent is used for communication between components.
Why is that? In order to pack to force
Use consideration:
- All the child components of a step bar component need to access its data, either with prop, parent is too troublesome, what if I want to extend to more deeply nested components? Therefore, we please provide provide and inject,
The benefits of this are:
- Interfaces between components are clearly defined, nested components components do not have to worry about changes to dependent components because they are accessible no matter how many layers are nested
- Provide/Inject enables easy cross-level access to ancestor component data without relying on vuex and other libraries
But keep in mind that there is no “perfect” way for components to communicate, provide has its drawbacks:
- As nesting levels become more complex, registered provide levels can make the data more complex and untracable (you don’t even know where the data came from!!).
- Properties are non-responsive, but if you pass in an object that can be listened to, then the object’s properties are still responsive.
- Injection of props is extensive, but not required for every component.
The Provide option allows us to specify the data/methods we want to Provide to future components
// Steps parent container
provide() {
return {
direction: this.direction,
active: this.active
}
}
Copy the code
Then in any child component, we can use itinject
Option to receive data/methods
// step child container
inject: ['direction'.'active']
inject: {
foo: { default: 'foo' } // You can set the default value
}
Copy the code
👉 provide and inject more knowledge
5. Provide and inject interesting points
1. Use this.$watch to solve the problem; Provide cannot be dynamically updated
Listening on $parent is risky: if your component hierarchy changes, $parent will not be what you want!! It is not recommended for use in this article, but only for use in tightly connected components
this.$watch('$parent.active'.val= > this.updateStatus(val),{immediate: true})
Copy the code
2. Pass in an object that can be listened to to resolve the problem; Provide cannot be dynamically updated
- Such as an object, or a function that returns an object
export default {
data() {
return {
level: {name: "Initialize"},
color: 'red',}},provide() {
return {
color: () = > { // This method needs to be called on the child component
return this.color
},
colorThat: this.// Pass this on to you
level: this.level
}
}
}
Copy the code
6. Implementation details of specific functions
The implementation principle of UI interface:
Steps principle: Actually very simple four components, center, cable positioning to left positioning to the middle. The last line hiding is that simple!
How do I listen for active changes in a parent component?
mounted() {
// Get the index of the current component
// Each component has its own unique UID that can be used as a component determination value
this.index = this.$parent.$children.findIndex(e= > e._uid === this._uid)
// Listen dynamically for active changes in the parent component
// ps: you can use my "5. Provide and inject interesting points" above
this.$watch('$parent.active'.val= > this.updateStatus(val), {immediate: true})},// Use this to change the state of the stpes component
updateStatus(parentActive) {
// If the current step state is passed in, use the current state
if (this.status) {
this.currentStatus = this.status
return
}
// A component is active if its index is equal to its parent's active value
// If the index of the current component is less than the active value of the parent component, the current component is completed
// If not, the default incomplete state
if (this.index === parentActive) {
this.currentStatus = 'active'
} else if (this.index < parentActive) {
this.currentStatus = 'finish'
} else {
this.currentStatus = 'inactive'}},Copy the code
How does stPES change states
🤔, this.inactiveColor, where do they come from
Props = props = props = props = props
computed: {
currentIcon() {
const statusMap = {
'inactive': {
color: this.inactiveColor,
icon: this.inactiveIcon
},
'active': {
color: this.activeColor,
icon: this.activeIcon
},
'finish': {
color: this.finishColor,
icon: this.finishIcon
},
'default': {
color: this.inactiveColor,
icon: this.inactiveIcon
}
}
return statusMap[this.currentStatus] || statusMap.default
}
}
Copy the code
<! - - - >
<div :class="'custom-step__head__'+direction">
<img :src="currentIcon.icon" />
<! Line -- -- -- >
<div :class="custom-step__line"></div>
</div>
<! -- Icon title -->
<div
:class="['custom-step__title']"
:style="{'color': currentIcon.color}"
>
{{ title }}
</div>
Copy the code
Six, the concluding
We can’t satisfy everyone and we can’t develop one “perfect” component, so don’t overdesign!! But not without design. At the beginning of the development, we want to do everything perfectly and in one step. Then I realized THAT I could never keep up with change.
See same logic function reject CV!! Instead, take action and abstract it into a reusable business component. How much better would your component abstraction have been if you hadn’t rejected the optimization in front of you? How much more maintainable is your code?
Thanks: Thank you for your support and encouragement at 4am on 8th
Seven, the source code
Steps components
<template>
<div
:class="'custom-steps--'+direction"
:style="{'background-color': background}"
>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
active: { // The current active step
type: Number.default: 0
},
direction: { // Container direction
type: String.validator(value) {
return ['horizontal'.'vertical'].find(e= > e === value)
},
default: 'horizontal'
},
background: { // The container background color
type: String.default: '#ffffff'}},provide() {
return {
direction: this.direction,
active: this.active
}
}
};
</script>
<style lang="scss" scoped>
.custom-steps--horizontal {
display: flex;
}
.custom-steps--vertical {
display: flex;
flex-direction: column;
}
</style>
Copy the code
Steps components
<template>
<div
:class="['custom-step__center', currentStatus]"
@click="stepClick"
>
<! - - - >
<div :class="'custom-step__head__'+direction">
<slot name="icon">
<img
:src="currentIcon.icon"
:class="['custom-status__icon', {'wait-icon': this.currentStatus === 'inactive'} ]"
/>
<! Line -- -- -- >
<div :class="['custom-step__line', {'active_horizontal': this.currentStatus === 'finish'}, direction]"></div>
<slot>
</div>
<! -- Icon title -->
<slot name="title">
<div
:class="['custom-step__title']"
:style="{'--active-color': currentIcon.color}"
>
{{ title }}
</div>
</slot>
</div>
</template>
<script>
export default {
props: {
title: { / / title
type: String.default: ' '
},
status: { // Use steps to set the status of the current step
type: String.default: ' '
},
inactiveColor: { // Inactive state color
type: String.default: '# 999999'
},
activeColor: { // Active state color
type: String.default: '# 333333'
},
finishColor: { // Finished state color
type: String.default: '# 999999'
},
inactiveIcon: { // Inactive status icon
type: String.default: require('./icons/inactive.png')},activeIcon: { // Activate the status icon
type: String.default: require('./icons/active.png')},finishIcon: { // Finished status icon
type: String.default: require('./icons/finish.png')}},inject: ['direction'.'active'].data() {
return {
index: -1.currentStatus: ' '}},computed: {
currentIcon() {
const statusMap = {
'inactive': {
color: this.inactiveColor,
icon: this.inactiveIcon
},
'active': {
color: this.activeColor,
icon: this.activeIcon
},
'finish': {
color: this.finishColor,
icon: this.finishIcon
},
'default': {
color: this.inactiveColor,
icon: this.inactiveIcon
}
}
return statusMap[this.currentStatus] || statusMap.default
}
},
mounted() {
// Parent is not recommended
this.index = this.$parent.$children.findIndex(e= > e._uid === this._uid)
this.$watch('$parent.active'.val= > this.updateStatus(val), {immediate: true})},methods: {
updateStatus(parentActive) {
// If the current step state is passed in, use the current state
if (this.status) {
this.currentStatus = this.status
return
}
// A component is active if its index is equal to its parent's active value
// If the index of the current component is less than the active value of the parent component, the current component is completed
// If not, the default incomplete state
if (this.index === parentActive) {
this.currentStatus = 'active'
} else if (this.index < parentActive) {
this.currentStatus = 'finish'
} else {
this.currentStatus = 'inactive'}},// Click the Step event
stepClick() {
this.$parent.$emit('click-step'.this.index)
},
}
};
</script>
<style lang="scss" scoped>
.custom-step__title {
font-size: 14px;
color: # 909399;
}
.custom-step__center {
flex-basis: 50%;
}
.custom-step__center .custom-step__line {
position: absolute;
right: -50%;
left: 50%;
height: 1px;
z-index: 0;
&.horizontal {
border-top: 2px solid #CAD6EE; }}.active_horizontal {
border-top: 2px solid #3D7BED ! important;
}
.custom-step__center:last-of-type .custom-step__line {
display: none;
}
.custom-step__center .custom-step__title {
text-align: center;
margin-top: 8px;
font-weight: 500;
}
.custom-step__center .custom-step__head__horizontal {
display: flex;
justify-content: center;
align-items: center;
position: relative; } // Dynamic title.inactive..active..finish{&.custom-step__title {
color: var(--active-color); }}.custom-status__icon {
width: 16px;
height: 16px;
z-index: 1;
}
.wait-icon {
padding: 4px;
box-sizing: border-box;
z-index: 1;
}
</style>
Copy the code