1. Demand analysis
First, a form should have the basic structure form-> formItem ->input
<template>
<div>
<Form :model="formData" :rules='rules'>
<FormItem label="Username" prop='username'>
<Input v-model='formData.username' placeholder="Please enter user name"></Input>
{{formData.username}}
</FormItem>
<FormItem label="Password" prop='password'>
<Input type='password' v-model='formData.password' placeholder="Please enter your password"></Input>
{{formData.password}}
</FormItem>
</Form >
</div>
</template>
<script>
import Input from "@/components/form/Input.vue";
import Form from "@/components/form/Form.vue";
import FormItem from "@/components/form/FormItem.vue";
export default {
components: {
Input,
FormItem,
Form
},
data(){
return{
formData: {username:' '.password:' '
},
rules: {
username: [{ required: true.message: "Please enter user name"}].password: [{ required: true.message: "Please enter your password"}}},}</script>
<style>
</style>
Copy the code
So the structure should look like this:
If it’s an containment relationship, if it’s an containment relationship, it’s going to be slot first so let’s put a slot first for the Form and formItem, so the code for them should look something like this
(Yes, you read that right, because form contains formItem, and formitem contains input. Therefore, it is ok to place two slots each at present)
/ / form. Vue and formitem. Vue
<template>
<div>
<slot></slot>
</div>
</template>
Copy the code
2. Realization of foundation construction
Input component: V-model of a custom component
The next step is to set up a custom Input component. The usual V-Model binds the custom Input component bidirectionally, rather than the Input of the native tag. The v-model is essentially a syntactic sugar that simplifies the process of value/@input. Therefore, to implement a custom Input component v-model, you need to implement :value/@input inside the component
See documentation: V-Model for custom components cn.vuejs.org/v2/guide/co…
/ / Input component
<template>
<div>
<! -- Custom component bidirectional binding: :value @input -->
<input :value="value" @input="onInput">
</div>
</template>
<script>
export default {
props: {
value: {
type: String.default: ' '}},methods: {
onInput(e) {
// Send an input event to the outfield
this.$emit('input', e.target.value)
}
},
}
</script>
// Used by the parent component
<Input v-model='username'></Input>
{{username}}
Copy the code
Ok to see the effect a custom component v-Model has been implemented
FormItem: Used to verify and display labels and error messages
1, implement label to display the name of the input box and error message, this is very simple, directly use props to pass the L attribute
/ / formitem code
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
<p v-if="error">{{error}}</p>/ / add</div>
</template>
<script>
export default {
data(){
return {
error:' '}},props: {label: {
type: String.default: ' '}},methods: {}}</script>
// Used by the parent component
<FormItem label="Username">
<Input v-model='username'></Input>
{{username}}
</FormItem>
Copy the code
3. Form component: maintain data, global check, submit data after passing
The Form component needs model management data and Rules to manage validation rules
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data(){
return{}},props: {model: {/ / must
type: Object.required: true,},rules: {
type: Object}},methods: {}}</script>
// Used by the parent component
<Form :model="formData" :rules='rules'>
<FormItem label="Username">
<Input v-model='formData.username'></Input>
{{formData.username}}
</FormItem>
<FormItem label="Password">
<Input v-model='formData.password'></Input>
{{formData.username}}
</FormItem></Form > data(){ return{ formData:{ username:'' }, rules: { username: [{ required: true, message: "Please enter user name"}], password: [{required: true, message: "Please enter password"}]}}},Copy the code
At this point, a basic form structure is set up
3.1, dojo.provide/inject
The Form component has the values of Model and Rules, and now needs to manage its data. How do you pass the data from the Model to the child components? Through structural analysis, here are the relationships between the parent and descendant components. Provide/Inject is used for data transfer. In this case, we just pass the “this” of the entire form
// The form component adds the provide code
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
provide(){
return {
form:this}},... }</script>
// The child formItem receives the form
export default {
inject: ["form"].data(){
return {
error:' '}},... }Copy the code
, $3.2 attr
For sub-components, there are usually other properties that need to be passed and that do not need to be written in props. You can use $attrs, such as the placeholder property of the input box, which will be passed directly to the input box
/ / <! -- v-bind="$attrs"
/ / input component
<input :type="type" :value="value" @input="onInput" v-bind="$attrs">
export default {
inheritAttrs: false.// inheritAttrs is set to false to avoid being set to the root element
}
Copy the code
The effect is as follows:
3. Data verification
First, the input component gets the value and notifies the formItem validation. We cannot emit events to formItem directly at this point because formItem has only one slot for display and slot has not yet become an input component. So we can use its parent component to emit validation events, and formItem itself to emit events to FormItem, because formItem only has a slot for display, slot has not yet become an input component, and there is no place to listen. So we can use its parent component to emit validation events, and formItem itself to emit events to FormItem, because formItem only has a slot for display, slot has not yet become an input component, and there is no place to listen. Therefore, we can use its parent component to emit the validation event, and formItem itself can listen for changes in the event. Formitem can get changes in the input
// Add $parent to the Input component
methods: {
onInput(e) {
// Send an input event to the outfield
this.$emit('input', e.target.value)
// Notify the parent to perform validate when the entered value changes
this.$parent.$emit('validate')}},/ / formitem components
// FormItem mountd listens on validate
mounted() {
this.$on("validate".() = > {
console.log('Input component is changing')}); },Copy the code
Now look at the effect: you can listen for changes to the input component
Ok, so how do you know which input changes and make a validation rule for them? In this case, you need prop. Formitem adds prop and passes prop to it when it’s used
Continue to modify components
/ / when used
<FormItem label="Username" prop='username'>
<Input v-model='formData.username' placeholder="Please enter user name"></Input>
{{formData.username}}
</FormItem>
// FormItem adds prop and validateFun ()
props:{
label: {
type: String.default: ' '
},
prop: {
type: String.default: ' '}},//methods
methods: {validateFun(){
/ / rules
const rules = this.form.rules[this.prop];
/ / the current value
const value = this.form.model[this.prop];
// Verify the description
const desc = { [this.prop]: rules };
console.log(rules,value,desc)
},
},
//mounted
mounted() {
this.$on("validate".() = > {
console.log('Input component is changing')
this.validateFun()
});
},
Copy the code
Look at the results:
Now that we can get the input value, we can start the validation process. The main library for validation is async-Validator
npm install async-validator
Copy the code
Using async-validator:github.com/yiminghe/as…
validateFun() {
/ / rules
const rules = this.form.rules[this.prop];
/ / the current value
const value = this.form.model[this.prop];
// Validates the description object
const desc = { [this.prop]: rules };
// Create Schema instance
const schema = new Schema(desc);
// Returns the checked value
return schema.validate({ [this.prop]: value }, errors= > {
if (errors) {
If errors is returned, the custom error text is displayed
this.error = errors[0].message;
} else {
// The check is successful
this.error = ""; }}); }Copy the code
Now let’s see what happens
Now that each formItem can be validated, you need to inform the Form component of the result for a Submit operation after the validation is successful
Analysis: The form needs to get all the formItem validation results.
// The form component validate method
validate(callback){
// First filter out items without prop that do not need validation. Then get validation results for all components. Call validateFun directly for sub-components
const validatResult = this.$children.filter(item= > item.prop).map(item= > item.validateFun())
console.log(validatResult)
// Since async-Validator is asynchronous, we need to use promise processing here
Promise.all(validatResult)
.then(() = > callback(true))
.catch(() = > callback(false));
},
// Submit when used
submit(){
this.$refs["form"].validate(valid= > {
if (valid) {
alert("success");
} else {
alert("fail");
return false; }})},Copy the code
So let’s see what happens
4. Robust thinking
At this point a basic form is complete, but here’s a thought. The input component can only send events to the parent component. What if the parent component of the input component is not a FormItem, but some other div element tag? ElemetUI and viewUI find that in this case, emitters. Js are used to distribute events github.com/ElemeFE/ele… So let’s introduce a file like that.
//broadcast: a top-down event,
function broadcast(componentName, eventName, params) {
// Tell all child elements to iterate over componentName
this.$children.forEach(child= > {
var name = child.$options.componentName;
if (name === componentName) {
// If the name of the child element is the same as the name passed in (so we need to add componentName to each child element)
child.$emit.apply(child, [eventName].concat(params));
} else{ broadcast.apply(child, [componentName, eventName].concat([params])); }}); }export default {
methods: {
// Bubble to find components with the same componentName and dispatch
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if(parent) { name = parent.$options.componentName; }}if(parent) { parent.$emit.apply(parent, [eventName].concat(params)); }},broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params); }}};Copy the code
As shown in the source code above, the Emitter component needs to pass in a componentName, so add componentName to each component to complete the function of Emitter
//formitem
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
<p v-if="error" style="color:red">{{error}}</p>
</div>
</template>
<script>
import Schema from "async-validator";
export default {
inject: ["form"].componentName:'FormItem'.// Because Emitter needs this field to traverse
data(){
return {
error:' '}},props: {label: {
type: String.default: ' '
},
prop: {
type: String.default: ' '}},methods: {validateFun(){
/ / rules
const rules = this.form.rules[this.prop];
/ / the current value
const value = this.form.model[this.prop];
// Verify the description
const desc = { [this.prop]: rules };
console.log(rules,value,desc)
// Create Schema instance
const schema = new Schema(desc);
// Returns the checked value
return schema.validate({ [this.prop]: value }, errors= > {
if (errors) {
If errors is returned, the custom error text is displayed
this.error = errors[0].message;
} else {
// The check is successful
this.error = ""; }}); }},mounted() {
this.$on("validate".() = > {
console.log('Input component is changing')
this.validateFun() }); }},</script>
Copy the code
5, summary
This article through the step by step analysis of the form needs to achieve the function, to complete a basic form form structure.