Episode 8: Implementation from scratch (input box, textarea component)
Location of this episode:
input component is an important tool for interaction, and it has the closest communication with users, so it has laid an important position in the component industry.
textarea is also a kind of input, if possible, this episode will be finished together, after all, it is a type of learning together will be a great harvest.
Ancient Cloud: "Components do not enclose input boxes, panic at an interview."
I. Introduction to v-model
If you are not familiar with the principle of the v-model instruction, it is suggested to learn the vue source code or read the relevant analysis articles. It is very important to know that the instruction is wonderful when there are more package components. Here I will briefly talk about his rules.
1: When the parent binds the v-model to the component, it actually passes the value variable to the component.
2: If your component defines value on props, you can get the value.
3: Whenever this.$emit("input",n) in a component sends an event outside, the n value is assigned to the value outside.
4: The reason for this design is that you have no right to change the incoming value in the component. If you want to change the value, you have to spit it out and let it change outside.
Okay, so much said, let's start fighting.
II. Basic structure
vue-cc-ui/src/components/Input/index.js
Old routine, unified export to adapt to the use of vue.use
import Input from './main/input.vue' Input.install = function(Vue) { Vue.component(Input.name, Input); }; export default Input
vue-cc-ui/src/components/Input/main/input.vue
- type: This attribute is important because it distinguishes input from textarea, and you can also specify number mode for input.
- The name is still bem
- v-bind="$attrs" explains this meaning, $attrs refers to attributes passed in by users, but does not include attributes received by props within our components, nor class style. It is written to allow users to pass many input native attributes. After all, we do not have to deal with all attributes to keep the components in possession. Primary function.
- Placement holder is basically abandoned by modern times. It can also be encapsulated as a specific component. It's too difficult to adjust the attribute, let alone we need the effect of Placement holder rotation, discoloration, click and so on.
- When vue writes events between lines, the event object will be passed to you in the form of $event. In fact, from the code point of view, when you use the keyword $event, the corresponding parameters will be assigned to the event object.
<template> <div class="cc-input"> <input type="text" class='cc-input__inner' :value="value" v-bind="$attrs" :placeholder="placeholder" @input="$emit('input',$event.target.value)"/> </div> </template>
props: { value: [String, Number], placeholder: [String, Number], type: { type: String, default: "text" } },
3. Enriching Events
- There are many kinds of events in the input box, they can give users better experience.
- For example, on the mobile phone side, the problem we met before the project was that when the user clicked on the input box, the keyboard would pop up, but the keyboard would pop up on the top of the input box. Some types of mobile phones would appear. Even if the input was completed by clicking, the input box would still be on top. Later, I used blur and F. The ocus event is compatible with this phone
- Many input boxes also use throttling and anti-shaking, such as relevant fuzzy matching for search.
- Some search-based pages require autofocus
<input :type="type" class='cc-input__inner' :value="value" v-bind="$attrs" :autofocus="autofocus" // Autofocus or not :placeholder="placeholder" @blur="blur($event.target.value)" @input="$emit('input',$event.target.value)" // Here's a little detail. This event binds two operations. // Not only triggers the focus event, but also sets the variable focus to true @focus="$emit('focus',$event.target.value);focus=true" @change="$emit('change',$event.target.value)" />
IV. Various States
- Disable, gray and disabled the mouse
- Read only, not ash, but read only.
Specific styles will be explained in detail later.
<input :type="type" :disabled="disabled" // Both are native attributes, but add styles :readonly="readonly" // All of them are native attributes without adding styles :class="{ 'cc-input--input__disabled':disabled }" />
5. Add status to the input box and attach icon options
- Many input boxes are filled with icons on the left and right sides, divided into icons on the left and icons on the right.
- The icon on the right allows you to enter text. The icon should have a corresponding click effect.
- When the component is disabled, icon also ashes it accordingly
<template> <div class="cc-input" :class="{ // Give each state a corresponding class 'cc-input__error':error, 'cc-input__normal':!disabled&&!normal, 'cc-input__abnormal':normal, 'cc-input__disabled':disabled, }" :style="{ // The input box has the effect of hovering and zooming in. Here you can adjust the zooming angle. Here's a graphic demonstration. 'transform-origin':`${transformOrigin} 0` }"> <nav v-if="leftIcon" class="cc-input__prefix is-left" // Return the corresponding click event @click="$emit('clickLeftIcon')"> <ccIcon :name='leftIcon' :color='iconColor' // Here the icon should also be grayed out. :disabled='disabled' /> </nav> <input :type="type" class='cc-input__inner' :value="value" v-bind="$attrs" :disabled="disabled" :readonly="readonly" :autofocus="autofocus" :placeholder="placeholder" :class="{ 'cc-input--input__disabled':disabled }" @blur="blur($event.target.value)" @input="$emit('input',$event.target.value)" @focus="$emit('focus',$event.target.value);focus=true" @change="$emit('change',$event.target.value)" /> <nav v-if="icon&&!clear" class="cc-input__prefix is-right" @click="$emit('clickRightIcon')"> <ccIcon :name="clear?'cc-close':icon" :color='iconColor' :disabled='disabled' /> // Allow users to insert various nodes <slot /> </nav> </div> </template>
Design sketch
6. Empty button
Now the input box basically has this empty button, after all, it can save time and is a good function.
When the user enters clear, he will decide whether to prohibit modification, whether there is value in the box, and whether it is in the hover state.
hover events are placed at the parent level
<div class="cc-input" @mouseenter="hovering = true" @mouseleave="hovering = false">
<nav v-if="showClear" class="cc-input__clear" @click="clickClear"> <ccIcon name="cc-close" :disabled='disabled' /> // Here is the unity of style. // For example, the user wrote a lot of text on the right button. // So the clear button is not easy to locate, so I wrote this station. <span style=" opacity: 0;"> <slot /> </span> </nav>
Clear the event and return to empty on ok
clickClear() { this.$emit("input", ""); this.$emit("change", ""); },
Determine whether to display
computed: { showClear() { if ( this.clear && // Open function !this.disabled && // Not disabled !this.readonly && // Not read-only this.value!== '' && // Not null (this.hovering || this.focus) // Focused or hover state )return true; return false; } },
vue-cc-ui/src/style/Input.scss
// Introducing the old four samples @import './common/var.scss'; @import './common/extend.scss'; @import './common/mixin.scss'; @import './config/index.scss'; // After all, this is a component written two months ago. It's not very good in naming, and it will be corrected uniformly in the future. @include b(input) { cursor: pointer; position: relative; align-items: center; display: inline-flex; // Direct flex will dominate the line background-color: white; transition: all .3s; @include b(input__inner) { border: none; flex: 1; width: 100%; font-size: 1em; padding: 9px 16px; &:focus { outline: 0; } // Writing like this is not very friendly to dyslexic reading. @include placeholder{ // Placement holder setting is a headache, see below color: $--color-input-placeholder; } }; @include b(input__prefix) { align-items: center; display: inline-flex; &:hover{transform: scale(1.1)} @include when(left) { padding-left:6px; } @include when(right) { padding-right:6px; } }; @include b(input__clear){ position: absolute; right: 24px; &:hover{ animation: size .5s infinite linear;} }; @include b(input--input__disabled){ @include commonShadow(disabled); }; @at-root { @include b(input__normal){ @include commonShadow($--color-black); &:hover { z-index: 6; transform: scale(1.2); } } @include b(input__error){ @include commonShadow(danger); } @include b(input__abnormal){ @include commonShadow($--color-black); } } }
Elementation does a good job
@mixin placeholder { &::-webkit-input-placeholder { @content; } &::-moz-placeholder { @content; } &:-ms-input-placeholder { @content; } }
textarea text field
Basic structure
- Open when the user type is textarea
- Copy the basic functions above and put them directly on it.
- Textarea CalcStyle: To set his width and height, after all, unlike input, he may need a large area.
- Users can set maximum height and minimum height
- Difficulty: If the user chooses to automatically adapt to the height, it will be troublesome. This component does not provide a native solution. In the first edition, I used to get its height to calculate, but there will be bug s in special cases. Finally, I referred to the implementation of element-ui, which I learned here.
<template> <div class="cc-input" ....> <template v-if="type !== 'textarea'"> <input :type="type" ..../> </template> <textarea v-else // You have to get this dom ref="textarea" class='cc-input__inner' :value="value" v-bind="$attrs" :disabled="disabled" :readonly="readonly" :autofocus="autofocus" :placeholder="placeholder" @blur="$emit('blur',$event.target.value)" @input="$emit('input',$event.target.value)" @focus="$emit('focus',$event.target.value)" @change="$emit('change',$event.target.value)" :style="{ width:rows, height:cols, ...textareaCalcStyle}" :class="{ 'cc-input--input__disabled':disabled, 'cc-input--input__autosize':autosize}" /> </div> </template>
For textarea to obtain its true height, dynamic assignment of height is carried out.
Let me say his principle is to make an element the same as a textarea object, get its rolling distance and height, calculate the total height, and then assign it to the real textarea. The highlight here is how to make the same dom, because the user may give the DOM different styles, different class es, various parent levels. The abdominal muscles also influence the pattern of this element.
// Personally, this life cycle function should be at the bottom, with a single responsibility to be maintained. mounted() { this.$nextTick(this.resizeTextarea); }
1: Determine whether autosize is an automatic height and component autosize
2: Does the user set the limit of maximum height and minimum height?
3: This function is only responsible for calcTextareaHeight.
resizeTextarea() { const { autosize, type } = this; if (type !== "autosize" || !autosize) return; const minRows = autosize.min; const maxRows = autosize.max; this.textareaCalcStyle = this.calcTextareaHeight( this.$refs.textarea, minRows, maxRows ); },
calcTextareaHeight
calcTextareaHeight(el, min, max) { // It's also a singleton model, just make an element. if (!window.hiddenTextarea) { window.hiddenTextarea = document.createElement("textarea"); document.body.appendChild(window.hiddenTextarea); } // Get his attributes. Get the attributes function. let [boxSizing, paddingSize, borderSize] = this.calculateNodeStyling(el); // Rolling distance let height = window.hiddenTextarea.scrollHeight; // Whether it's a weird box model or not, calculate separately if (boxSizing === "border-box") { height = height + borderSize; } else { height = height - paddingSize; } // Clean up in time so that users can't see this element window.hiddenTextarea.parentNode && window.hiddenTextarea.parentNode.removeChild(window.hiddenTextarea); window.hiddenTextarea = null; if (min && height < min) height = min; else if (max && height > max) height = max; return { height: height + "px" }; }
calculateNodeStyling
calculateNodeStyling(el) { // Analog elements simulate real elements by input of values window.hiddenTextarea.value = this.value; const style = window.getComputedStyle(el); const boxSizing = style.getPropertyValue("box-sizing"); const paddingTop = style.getPropertyValue("padding-top"); const paddingBottom = style.getPropertyValue("padding-bottom"); const borderTopWidth = style.getPropertyValue("border-top-width"); const borderBottomWidth = style.getPropertyValue("border-bottom-width"); const contextStyle = this.CONTEXT_STYLE.map( name => `${name}:${style.getPropertyValue(name)}` ).join(";"); window.hiddenTextarea.setAttribute( "style", `${contextStyle};${this.HIDDEN_STYLE}` ); return [ boxSizing, parseInt(paddingBottom) + parseInt(paddingTop), parseInt(borderBottomWidth) + parseInt(borderTopWidth) ]; },
this.CONTEXT_STYLE data used above is a list of styles
data() { return { focus: false, // Focus out of focus of listening input box hovering: false, textareaCalcStyle: {}, CONTEXT_STYLE: [ "width", "font-size", "box-sizing", "line-height", "padding-top", "font-family", "font-weight", "text-indent", "border-width", "padding-left", "padding-right", "letter-spacing", "padding-bottom", "text-rendering", "text-transform" ] }; },
So far, it's hard to finish this component.
end
If you want to be all-inclusive, there is no simple component. Every component on element is worth learning from.
In fact, many principles can be learned more quickly after they are understood. Recently, I have taken time to show you the principles of vue, vue-router vuex and so on. I hope it will be helpful to all of you. I can only say that the endless learning sea is the shore.
I hope you can make progress together and realize your self-worth!!
Next episode is going to talk about counter
For more interesting results, please pay attention to personal blogs: Link Description
github: Link Description