A mature form
Forms forms, you’re old enough to learn:
- Dynamic rendering
- Supports single, double, and multiple columns
- Support layout adjustment
- Support for form validation
- Support to adjust the display order
- Displays required components based on component values
- Support item extension components
- Models can be created automatically
This form control is a secondary encapsulation based on The Elform of Element-Plus, so first of all, thank Element-Plus for providing such a powerful UI library. I used jQuery to make a similar UI before, but it was very troublesome, neither beautiful, maintainability, scalability is poor. A lot of ideas don’t work (technology is limited).
Now it’s time to stand on the shoulders of giants and realize your ideas.
Implementing dynamic rendering
Put all the required properties of the form into JSON and load them with require or AIOXs (which can be hot updated) to achieve dynamic rendering. For example, to add and modify the company information, you only need to load the JSON required by the company information. To add and modify employee information, you only need to load the JSON required by employee information.
In short, just load the JSON you need, you don’t need to hand-lift the code over and over again.
So what does this amazing JSON look like? The file is a bit long, so if you look at the screenshot, it will be clearer.
There are several additional features:
- Supports merging in a single row.
In the case of a single line, some of the short controls take up more space, and we can combine several small controls into a single line.
- Supports multi-line extensions.
In the case of multiple rows, some long controls take up more space, so we can set them to take up more space.
- Automatically create the model required by the form.
You don’t need to write the model manually.
Implement a multi-row, multi-column form
Thanks again to El-Form, it’s really powerful, it’s nice to look at, it offers validation, and a lot more. It just seems to be horizontal or vertical. Can we have multiple rows and columns? It doesn’t seem to be directly provided.
We know that el-Row and EL-Col can implement multiple rows and columns, so can we combine them? The official website is not straightforward, I have all kinds of search, good to find. (Ok, table has been around for a while)
A little trick here is that you only need one el-row, you can have more than one el-col, so that when a row is full, it automatically goes to the next row.
<el-form
ref="form"
:inline="false"
class="demo-form-inline"
:model="formModel"
label-suffix=":"
label-width="130px"
size="mini"
>
<el-row>
<! -- Instead of loop through row, loop through col. -->
<el-col
v-for="(ctrId, index) in formColSort"
:key="'form_'+index"
:span="formColSpan[ctrId]"
>
<el-form-item :label="getCtrMeta(ctrId).label">
<! -- The form item component is used as a dynamic component -->
<component
:is="ctlList[getCtrMeta(ctrId).controlType]"
v-model="formModel[getCtrMeta(ctrId).colName]"
:meta="getCtrMeta(ctrId)"
@myChange="mySubmit">
</component>
</el-form-item>
</el-col>
</el-row>
</el-form>
Copy the code
- formColSort
An array of component ids that determines which components to display and in what order.
- v-for
Iterate through formColSort to get the component ID, then get the span corresponding to the ID (to determine the placeholder) and the meta required by the component.
- formColSpan
An array that holds component placeholders. According to the el-Col span of 24 grids.
- getCtrMeta(ctrId)
Gets the meta of a component based on its ID. Why write a function? Since the attributes of the model do not allow braces, we had to write a function. Why not calculate the properties? Calculated properties do not seem to pass parameters.
- component :is=”xxx”
Vue provides dynamic components that make it easy to load different types of subcomponents.
- ctlList
Component dictionary, which converts component types into corresponding component labels.
Such a V-for takes care of a number of things, such as single-column, multi-column, component sorting, component placeholders, and displaying different components depending on the user’s choice. In fact, it changes the composition and order of component ids in formColSort.
Automatic model creation
I am quite lazy, is it a little trouble to lift the model by hand? It would be nice if I could get it automatically, so I wrote this function.
// Create a V-Model based on the form element meta
const createModel = () = > {
// Create module according to meta
for (const key in formItemMeta) {
const m = formItemMeta[key]
// Set property values based on the control type
switch (m.controlType) {
case 100: / / class text
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
case 130:
case 131:
formModel[m.colName] = ' '
break
case 110: / / date
case 111: // Date time
case 112: / / years
case 114: / / year
case 113: / / in weeks
formModel[m.colName] = null
break
case 115: // Any time
formModel[m.colName] = '00:00:00'
break
case 116: // Select the time
formModel[m.colName] = '00:00'
break
case 120: / / digital
case 121:
formModel[m.colName] = 0
break
case 150: / / check
case 151: / / switch
formModel[m.colName] = false
break
case 153: / / radio set
case 160: // Select a drop-down list
case 162: // Pull down linkage
formModel[m.colName] = null
break
case 152: / / multiple groups
case 161: // Pull down multiple selections
formModel[m.colName] = []
break
}
// See if the default values are set
if (typeofm.defaultValue ! = ='undefined') {
switch (m.defaultValue) {
case ' ':
break
case '{}':
formModel[m.colName] = {}
break
case '[]':
formModel[m.colName] = []
break
case 'date':
formModel[m.colName] = new Date(a)break
default:
formModel[m.colName] = m.defaultValue
break}}}// Synchronize the parent component's V-model
context.emit('update:modelValue', formModel)
return formModel
}
Copy the code
It’s much easier to set the properties of the Model based on the type and default values.
Create the model selected by the user
This is the model after the user selects an option and the components of the form respond to changes. In my plan, I needed such a simple model, so I wrote another function
// Create the corresponding model according to the user's options
const createPartModel = (array) = > {
// Delete attributes first
for (const key in formPartModel) {
delete formPartModel[key]
}
// Create a new attribute
for (let i = 0; i < array.length; i++) {
const colName = formItemMeta[array[i]].colName
formPartModel[colName] = formModel[colName]
}
}
Copy the code
And then you get a neat model.
Multi-column forms
This is the most complex, divided into two cases: a single column of a squeeze, a multi-column grab position.
Single row
One feature of a single-column form is that it has a loose row, so sometimes two components need to be displayed in a row, and other components need to be displayed in a row. How do you adjust this?
Here’s a setup:
- If one component is in a row, call it 1
- If two components are squeezed in a row, we call this -2
- If three components are squeezed in a row, we call this -3
By analogy, a maximum of -24 is theoretically supported, although in practice there seems to be no such wide display.
After this recording, we can determine that the number ≥1 is span=24, and the negative number is divided by 24, and the number is span. Remember to take integers, of course.
Why do we use negative numbers? The purpose is to distinguish multiple column adjustments.
Multiple columns
There is a problem after tuning too much, it looks the same as after single-column adjustment.
Multi-column forms have a feature that one grid is too small, and some components are too long to fit in, so this component must be used for the following grid.
So let’s make a setting:
- If one component takes up one cell, let’s call it one
- If a component occupies two compartments, call it 2
- If a component occupies three Spaces, call it 3
And so on.
So once we’ve done that, we can say, if it’s less than or equal to 1, let’s call it 24 over the columns, and if it’s greater than 1, let’s call it 24 over the columns times n. This is fine, it is compatible with single-column Settings, do not have to adjust the Settings because a single column becomes multiple columns. There is just a small trouble, occupy too much grid, will be extracted crowded to the next line, and the line will appear “empty”. We’ll have to adjust this manually for now. After all, which field is in front, or need to manually set.
An analysis like a tiger, a look at a few lines of code.
// Set formColSpan according to colCount
const setFormColSpan = () = > {
const formColCount = formMeta.formColCount / / the number of columns
const moreColSpan = 24 / formColCount // How many slices are in a grid
if (formColCount === 1) {
// the value of a column
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount >= 1) {
// Select * from a single column
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount < 0) {
// In the squeeze case, 24 divided by the number of shares
formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
}
}
}
} else {
// Multiple columns
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount < 0 || m.colCount === 1) {
// There are several columns
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount > 1) {
// The number of columns * the number of copies
formColSpan[m.controlId] = moreColSpan * m.colCount
}
}
}
}
}
Copy the code
Finally, you can set the number of columns dynamically:
The corresponding component is displayed based on the user’s selection
This is also a much-needed feature, otherwise the adaptability of dynamically rendered form controls would be limited. You can change the component ID in formColSort. We set a watch to listen for changes in component values, and then set the required component ID to formColSort.
// Monitor component value changes, adjust component display and display order
if (typeofformMeta.formColShow ! = ='undefined') {
for (const key in formMeta.formColShow) {
const ctl = formMeta.formColShow[key]
const colName = formItemMeta[key].colName
watch(() = > formModel[colName], (v1, v2) = > {
if (typeof ctl[v1] === 'undefined') {
// No Settings, display default components
setFormColSort()
} else {
// Display the components as specified
setFormColSort(ctl[v1])
// Set part of the model
createPartModel(ctl[v1])
}
})
}
}
Copy the code
Because there may be more than one component to listen on, a loop is made so that you can listen on all the components you need.
See the effect [Video 2]
The complete code
The above code is a bit messy, so here’s the overview.
- el-form-manage.js
The management class for the form component is separate so that it can support other UI libraries, such as ANTDV
import { reactive, watch } from 'vue'
/** * Form management class ** Create V-Model ** adjust the number of columns ** merge */
const formManage = (props, context) = > {
// Define the complete V-model
const formModel = reactive({})
// Define a local model
const formPartModel = reactive({})
// Determine how many grids a component occupies
const formColSpan = reactive({})
// Define the sort basis
const formColSort = reactive([])
// Get the form meta
const formMeta = props.meta
console.log('formMeta', formMeta)
// Form element meta
const formItemMeta = formMeta.itemMeta
// Form validation meta, alternate
// const formRuleMeta = formMeta.ruleMeta
// Create a V-Model based on the form element meta
const createModel = () = > {
// Create module according to meta
for (const key in formItemMeta) {
const m = formItemMeta[key]
// Set property values based on the control type
switch (m.controlType) {
case 100: / / class text
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
case 130:
case 131:
formModel[m.colName] = ' '
break
case 110: / / date
case 111: // Date time
case 112: / / years
case 114: / / year
case 113: / / in weeks
formModel[m.colName] = null
break
case 115: // Any time
formModel[m.colName] = '00:00:00'
break
case 116: // Select the time
formModel[m.colName] = '00:00'
break
case 120: / / digital
case 121:
formModel[m.colName] = 0
break
case 150: / / check
case 151: / / switch
formModel[m.colName] = false
break
case 153: / / radio set
case 160: // Select a drop-down list
case 162: // Pull down linkage
formModel[m.colName] = null
break
case 152: / / multiple groups
case 161: // Pull down multiple selections
formModel[m.colName] = []
break
}
// See if the default values are set
if (typeofm.defaultValue ! = ='undefined') {
switch (m.defaultValue) {
case ' ':
break
case '{}':
formModel[m.colName] = {}
break
case '[]':
formModel[m.colName] = []
break
case 'date':
formModel[m.colName] = new Date(a)break
default:
formModel[m.colName] = m.defaultValue
break}}}// Synchronize the parent component's V-model
context.emit('update:modelValue', formModel)
return formModel
}
// Run it once
createModel()
// Submit the model to the parent component
const mySubmit = (val, controlId, colName) = > {
context.emit('update:modelValue', formModel)
// Sync to some models
if (typeofformPartModel[colName] ! = ='undefined') {
formPartModel[colName] = formModel[colName]
}
context.emit('update:partModel', formPartModel)
}
// Create the corresponding model according to the user's options
const createPartModel = (array) = > {
// Delete attributes first
for (const key in formPartModel) {
delete formPartModel[key]
}
// Create a new attribute
for (let i = 0; i < array.length; i++) {
const colName = formItemMeta[array[i]].colName
formPartModel[colName] = formModel[colName]
}
}
// Set formColSpan according to colCount
const setFormColSpan = () = > {
const formColCount = formMeta.formColCount / / the number of columns
const moreColSpan = 24 / formColCount // How many slices are in a grid
if (formColCount === 1) {
// the value of a column
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount >= 1) {
// Select * from a single column
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount < 0) {
// In the squeeze case, 24 divided by the number of shares
formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
}
}
}
} else {
// Multiple columns
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount < 0 || m.colCount === 1) {
// There are several columns
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount > 1) {
// The number of columns * the number of copies
formColSpan[m.controlId] = moreColSpan * m.colCount
}
}
}
}
}
// Run it once
setFormColSpan()
// Sets the display order of components
const setFormColSort = (array = formMeta.colOrder) = > {
formColSort.length = 0formColSort.push(... array) }// Run it first
setFormColSort()
// Monitor component value changes, adjust component display and display order
if (typeofformMeta.formColShow ! = ='undefined') {
for (const key in formMeta.formColShow) {
const ctl = formMeta.formColShow[key]
const colName = formItemMeta[key].colName
watch(() = > formModel[colName], (v1, v2) = > {
if (typeof ctl[v1] === 'undefined') {
// No Settings, display default components
setFormColSort()
} else {
// Display the components as specified
setFormColSort(ctl[v1])
// Set part of the model
createPartModel(ctl[v1])
}
})
}
}
return {
/ / object
formModel, // v-model createModel()
formPartModel, // Model of the component selected by the user
formColSpan, // Determine the component placeholder
formColSort, // Determine component ordering
/ / function
createModel, / / create v - model
setFormColSpan, // Set the component placeholder
setFormColSort, // Set component ordering
mySubmit / / submit}}export default formManage
Copy the code
- el-form-map.js
Dictionaries required by dynamic components
import { defineAsyncComponent } from 'vue'
/** * Register controls in the component with ** text ** * eltext single line text, phone, email, search ** * elarea multi-line text ** * elURL ** number ** elnumber ** elrange slider ** Date ** * elDate Date, date, year, week, year ** * elTime Time ** Select ** * elcheckBox select ** * ELswitch switch ** * elcheckboxs multiple select groups ** * elradios Option group ** * elSelect drop - down select */
const formItemList = {
// Text class defineComponent
eltext: defineAsyncComponent(() = > import('./t-text.vue')),
elarea: defineAsyncComponent(() = > import('./t-area.vue')),
elurl: defineAsyncComponent(() = > import('./t-url.vue')),
/ / digital
elnumber: defineAsyncComponent(() = > import('./n-number.vue')),
elrange: defineAsyncComponent(() = > import('./n-range.vue')),
// Date and time
eldate: defineAsyncComponent(() = > import('./d-date.vue')),
eltime: defineAsyncComponent(() = > import('./d-time.vue')),
// Select, switch
elcheckbox: defineAsyncComponent(() = > import('./s-checkbox.vue')),
elswitch: defineAsyncComponent(() = > import('./s-switch.vue')),
elcheckboxs: defineAsyncComponent(() = > import('./s-checkboxs.vue')),
elradios: defineAsyncComponent(() = > import('./s-radios.vue')),
elselect: defineAsyncComponent(() = > import('./s-select.vue')),
elselwrite: defineAsyncComponent(() = > import('./s-selwrite.vue'))}/** * a dictionary of dynamic components, easy to set control */ inside the V-for loop
const formItemListKey = {
/ / class text
100: formItemList.elarea, // Multi-line text
101: formItemList.eltext, // Single line of text
102: formItemList.eltext, / / password
103: formItemList.eltext, / / phone
104: formItemList.eltext, / / email
105: formItemList.elurl, // url
106: formItemList.eltext, / / search
/ / digital
120: formItemList.elnumber, / / array
121: formItemList.elrange, / / the slider
// Date and time
110: formItemList.eldate, / / date
111: formItemList.eldate, // Date + time
112: formItemList.eldate, / / years
113: formItemList.eldate, / / in weeks
114: formItemList.eldate, / / year
115: formItemList.eltime, // Any time
116: formItemList.eltime, // Select a fixed time
// Select, switch
150: formItemList.elcheckbox, / / check
151: formItemList.elswitch, / / switch
152: formItemList.elcheckboxs, / / multiple groups
153: formItemList.elradios, / / radio set
160: formItemList.elselect, / / the drop-down
161: formItemList.elselwrite, // Pull down multiple selections
162: formItemList.elselect // Pull down linkage
}
export default {
formItemList,
formItemListKey
}
Copy the code
- el-form-div.vue
The code template for the form control
<div >
<el-form
ref="form"
:inline="false"
class="demo-form-inline"
:model="formModel"
label-suffix=":"
label-width="130px"
size="mini"
>
<el-row>
<! -- Instead of loop through row, loop through col. -->
<el-col
v-for="(ctrId, index) in formColSort"
:key="'form_'+index"
:span="formColSpan[ctrId]"
>
<el-form-item :label="getCtrMeta(ctrId).label">
<! -- The form item component is used as a dynamic component -->
<component
:is="ctlList[getCtrMeta(ctrId).controlType]"
v-model="formModel[getCtrMeta(ctrId).colName]"
:meta="getCtrMeta(ctrId)"
@myChange="mySubmit">
</component>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
Copy the code
js
import { watch } from 'vue'
import elFormConfig from '@/components/nf-el-form/el-form-map.js'
import formManage from '@/components/nf-el-form/el-form-manage.js'
export default {
name: 'el-form-div'.components: {
...elFormConfig.formItemList
},
props: {
modelValue: Object.partModel: Object.meta: Object
},
setup (props, context) {
// Control dictionary
const ctlList = elFormConfig.formItemListKey
// Form management class
const {
formModel, // Create Model based on meta
formColSpan, // Create span according to meta
formColSort,
setFormColSpan,
setFormColSort, // Set component ordering
mySubmit
} = formManage(props, context)
// listen for column number changes
watch(() = > props.meta.formColCount, (v1, v2) = > {
setFormColSpan()
})
/ / listen to reload
watch(() = > props.meta.reload, (v1, v2) = > {
setFormColSpan()
setFormColSort()
})
// Listen for component value changes,
// Get component meta by ID, because model does not support [] nesting
const getCtrMeta = (id) = > {
return props.meta.itemMeta[id] || {}
}
return {
formModel,
formColSpan,
formColSort,
ctlList,
getCtrMeta,
mySubmit
}
}
}
Copy the code
This is much easier because the JS code that implements the specific functionality is separated out. Either as child components or as separate JS files. This is mainly responsible for re-rendering the form component.
Form validation
This uses the validation functionality provided by el-Form. The validation of the el-Form has not been generalized yet, because the validation data needs to be written into JSON and then read and set. So it’s gonna be easy. It just takes a little time.
Support extension Components
Native components are certainly not enough because user needs are constantly changing, so how do you add new components to the form controls? You can use the interface definition to encapsulate the components to meet the requirements, and then make a map dictionary, can be set in.
Because the interface is unified, it can adapt to the call of form controls.
The easy way is to modify the two JS files directly. It can also be passed in via properties if it is not easy to modify. I haven’t worked out the details yet, but it doesn’t seem too difficult.
The source code
Github.com/naturefwvue…