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…