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…