A mature form
Form form, you’ve grown up, you’ve got to learn:
- Dynamic rendering
- Supports single, double, and multiple columns
- Adjusting the layout
- Support for form validation
- Support to adjust the order of arrangement (display)
- Displays required components based on component values
- Support for the Item extension component
- Models can be created automatically
This form control is a secondary encapsulation based on Elemental-Plus’ el-Form, so first of all, thank Elemental-Plus for providing such a powerful UI library. I have done similar with jQuery before, but it is very cumbersome, not beautiful, maintainability and extensibility are also poor. Many of the ideas are not implemented (technology is limited). Now it’s time to stand on the shoulders of giants and realize your idea.
Dynamic rendering
You can dynamically render the form by putting all the required properties in JSON and loading them with require (convenient) or AIOXS (hot update). For example, to realize the addition and modification of corporate information, you only need to load the JSON required by corporate 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, and you don’t need to hand code over and over again.
So what does this amazing JSON look like? The file is a little long, just look at the screenshot, it’s clearer.
There are several additional features:
- Supports merges in a single row.
In the case of a single line, some of the shorter controls will take up more space, we can combine multiple small controls into a single line.
- Support for multi-line extensions.
In the case of multiple rows, some long controls take up more space, so we can set it to take up more cells.
- Automatically create the model required by the form.
You don’t have to write the model manually.
Implement a form with multiple rows and columns
Thanks again to El-Form, it’s really powerful, not only nice to look at, but it also provides validation and lots of other features. It just seems like it has to be horizontally or vertically. So can we have more rows and more columns? It doesn’t seem to be directly provided.
We know that el-Row and El-col can have multiple rows and columns, so what about combining them? The official website also does not say, I all kinds of harm to find, but also found. (Ok, actually the table that has been tossing around for a while)
It’s a combination of the two, there’s a trick here, you only need one for el-row, you can have more than one for el-col, so when a row is full, it automatically moves 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>
<! Loop col directly instead of row. If no row is available, it will automatically feed down the line. -->
<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 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 the formColSort to get the component ID, then get the span for the ID (determine the placeholder) and the meta that the component needs.
- formColSpan
An array that holds component placeholders. According to el-col span 24 lattice Settings.
- getCtrMeta(ctrId)
Gets the meta of the component based on the component ID. Why write a function? Since the model property does not allow brackets, we have to write a function. Why don’t you compute properties? Calculated properties do not seem to pass parameters.
- component :is=”xxx”
Vue provides dynamic components that make it easy to load sub-components of different types.
- ctlList
Component dictionary, which turns component types into corresponding component labels.
This v-for handles many things, such as single-column, multi-column, component sorting, component placeholder, and displaying different components according to the user’s choice. It simply changes the composition and order of component ids in formColSort.
Automatic model creation
I’m lazy. Is it a bit of trouble for me to masturbate the model? It would be nice to get that automatically, so I wrote this function.
// Create a v-model based on the form element meta
const createModel = () = > {
// Create a module based on meta
for (const key in formItemMeta) {
const m = formItemMeta[key]
// Set the property value according to 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 and 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 option
case 162: // Pull down linkage
formModel[m.colName] = null
break
case 152: / / multiple groups
case 161: // Drop down multiple selection
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 v-model of the parent component
context.emit('update:modelValue', formModel)
return formModel
}
Copy the code
It is easier to set the properties of the Model by type and default values.
Creates the model that the user selects
When the user selects an option, the form component responds to the changed Model. In my plan, I needed a simple model like this, so I wrote another function
// Create a model based on user 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
So you have a neat model.
A multi-column form
This one is the most complicated, and it can be divided into two cases: single-column jostling and multi-column jostling.
Single row
One feature of a single column form is that one row is relatively loose, so sometimes two components need to be displayed in a row, and others still one component per row, so how to adjust?
Here’s a setting:
- One component in a row, I call it 1
- If you squeeze two components into a row, that’s minus 2
- If you squeeze three components into one row, you call it minus three
Along the way, the theoretical maximum support is -24, although in practice it doesn’t seem to be that wide.
So once we’ve done that, we can say, well, if we have a number greater than or equal to 1, we can write span=24, and if we divide negative numbers by 24, we get span. Remember to use integers, of course.
Why do we use negative numbers? Just to separate multiple columns.
Multiple columns
After tuning too much, I found a problem. It seems to be the same as after single column adjustment.
One of the characteristics of a multi-column form is that one of the cubicles is too small for some components to fit into, and that component has to grab the cubicles behind it.
So let’s make a setting:
- If one component occupies one space, I’ll call it one again
- If a component occupies two Spaces, it is denoted as two
- If a component has three Spaces, it’s called three
And so on.
And then we can say, if it’s less than or equal to 1, it’s 24 over the number of columns, and if it’s greater than 1, it’s 24 over the number of columns times n. That’s fine, and it’s compatible with single-column Settings, so you don’t have to adjust the Settings as a single column changes to multiple columns. There’s just a small problem. If it takes up too much space, it will be squeezed to the next row, and there will be a “gap” in the row. This will be manually adjusted for the time being. After all, you need to manually set which field comes first.
A ferocious analysis, a few lines of code.
// Set formColSpan according to the colCount configuration
const setFormColSpan = () = > {
const formColCount = formMeta.formColCount / / the number of columns
const moreColSpan = 24 / formColCount // How many parts does a grid have
if (formColCount === 1) {
// One column case
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount >= 1) {
// The number of columns is only 24
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount < 0) {
// In the case of a squeeze, 24 divided by the number of shares
formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
}
}
}
} else {
// In the case of 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 more columns than one column
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount > 1) {
// Multiple columns, number of cells * copies
formColSpan[m.controlId] = moreColSpan * m.colCount
}
}
}
}
}
Copy the code
Look at the effect of the final, can dynamically set the number of columns: www.zhihu.com/zvideo/1347…
According to the user’s selection, the corresponding component is displayed
This is also a much-needed feature, otherwise the adaptability of dynamically rendered form controls would be limited. Change the component ID in formColSort. We set up a watch to listen for component values, and then set the required component ID to formColSort.
// Listen for changes in component values and adjust the display order of components
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') {
// Display the default component without setting
setFormColSort()
} else {
// Display components as specified
setFormColSort(ctl[v1])
// Set part of the model
createPartModel(ctl[v1])
}
})
}
}
Copy the code
Since there may be more than one component to listen on, I make a loop so that I can listen on all the required components.
See www.zhihu.com/zvideo/1347…
The complete code
The above code is a bit messy, but here’s a general overview.
- el-form-manage.js
The form component management class is taken out separately so that other UI libraries, such as ANTDV, can be supported
import { reactive, watch } from 'vue'
/** * Form management class ** create v-Model ** adjust column number ** merge */
const formManage = (props, context) = > {
// Define the full V-model
const formModel = reactive({})
// Define the local model
const formPartModel = reactive({})
// Determine how many cells a component occupies
const formColSpan = reactive({})
// Define the sorting criteria
const formColSort = reactive([])
// Get the form meta
const formMeta = props.meta
console.log('formMeta', formMeta)
// The form element meta
const formItemMeta = formMeta.itemMeta
// Form validation meta, standby
// const formRuleMeta = formMeta.ruleMeta
// Create a v-model based on the form element meta
const createModel = () = > {
// Create a module based on meta
for (const key in formItemMeta) {
const m = formItemMeta[key]
// Set the property value according to 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 and 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 option
case 162: // Pull down linkage
formModel[m.colName] = null
break
case 152: / / multiple groups
case 161: // Drop down multiple selection
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 v-model of the parent component
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)
// Synchronize to part of the model
if (typeofformPartModel[colName] ! = ='undefined') {
formPartModel[colName] = formModel[colName]
}
context.emit('update:partModel', formPartModel)
}
// Create a model based on user 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 the colCount configuration
const setFormColSpan = () = > {
const formColCount = formMeta.formColCount / / the number of columns
const moreColSpan = 24 / formColCount // How many parts does a grid have
if (formColCount === 1) {
// One column case
for (const key in formItemMeta) {
const m = formItemMeta[key]
if (typeof m.colCount === 'undefined') {
formColSpan[m.controlId] = moreColSpan
} else {
if (m.colCount >= 1) {
// The number of columns is only 24
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount < 0) {
// In the case of a squeeze, 24 divided by the number of shares
formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
}
}
}
} else {
// In the case of 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 more columns than one column
formColSpan[m.controlId] = moreColSpan
} else if (m.colCount > 1) {
// Multiple columns, number of cells * copies
formColSpan[m.controlId] = moreColSpan * m.colCount
}
}
}
}
}
// Run it once
setFormColSpan()
// Set the display order of components
const setFormColSort = (array = formMeta.colOrder) = > {
formColSort.length = 0formColSort.push(... array) }// Run it first
setFormColSort()
// Listen for changes in component values and adjust the display order of components
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') {
// Display the default component without setting
setFormColSort()
} else {
// Display 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 the sort of components
/ / function
createModel, / / create v - model
setFormColSpan, // Set the component placeholder
setFormColSort, // Set component sort
mySubmit / / submit}}export default formManage
Copy the code
- el-form-map.js
Dictionary required by dynamic components
import { defineAsyncComponent } from 'vue'
/** register controls with ** text ** elText single-line text, phone, mail, search ** elArea ** multi-line text ** elURL ** number ** elNumber ** elrange slider ** Date ** * elDate date, year, year, week, year ** * eltime ** Select ** elCheckbox select ** * elSwitch ** * ElCheckBoxs multi-choice group ** * elradios Radio group ** * ELSELECT Drop-down list selection */
const formItemList = {
// 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 for setting controls */ inside v-for loops
const formItemListKey = {
/ / class text
100: formItemList.elarea, // Multiline text
101: formItemList.eltext, // One 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, // Drop down multiple selection
162: formItemList.elselect // Pull down linkage
}
export default {
formItemList,
formItemListKey
}
Copy the code
- el-form-div.vue
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>
<! Loop col directly instead of row. If no row is available, it will automatically feed down the line. -->
<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 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 a Model based on meta
formColSpan, // Create a span based on meta
formColSort,
setFormColSpan,
setFormColSort, // Set component sort
mySubmit
} = formManage(props, context)
// Listen for changes in column count
watch(() = > props.meta.formColCount, (v1, v2) = > {
setFormColSpan()
})
/ / listen to reload
watch(() = > props.meta.reload, (v1, v2) = > {
setFormColSpan()
setFormColSort()
})
// Listen for changes in component values,
// Get the meta of the component based on its ID, because the model does not support [] nesting
const getCtrMeta = (id) = > {
return props.meta.itemMeta[id] || {}
}
return {
formModel,
formColSpan,
formColSort,
ctlList,
getCtrMeta,
mySubmit
}
}
}
Copy the code
Here it is much simpler because the JS code that implements the specific function is separated out. Either as a child component or as a separate JS file. The main responsibility here is to re-render the form components.
Form validation
This uses the validation provided by el-Form. At present, el-form validation has not been concluded, because the data used for this validation needs to be written into JSON, and then read out and set. So it’s gonna be easy. It’s just gonna take some time.
Support for extension components
Built-in components are definitely not enough, because user needs are constantly changing, so how can new components be added to form controls? You can encapsulate the required components according to the interface definition, and then create a map dictionary that you can set up.
Because the interface is uniform, it can accommodate calls to form controls.
The easy way is to modify two JS files directly. If it is not convenient to change, you can also pass in the properties. I haven’t worked out the details yet, but it doesn’t seem too difficult.
The source code
Github.com/naturefwvue…