background

Forms are arguably one of the most frequently encountered elements in front-end development. In the development of daily forms, there are V-IF conditional rendering, full screen magic number enumerated values, and complex linkage interaction between forms, which often makes a seemingly simple form become even more bloated.

Form relationship and reset often scattered in various function method, as demand expansion and change constantly, making the form of the coupling between the complexity rising, for the subsequent developers, it is difficult to clear understanding of the business logic implied in form quickly and correlation, this makes the form is very not easy to maintain.

Configuration form building

In business development, the ultimate purpose of using forms is to submit data in a specific format. Is there a way to configure some kind of data structure that clearly expresses the parameters, control types, and linkage relationships of individual form items?

Configure the form through JSON

Starting with configuration, we think of form development as configuring some key and value mappings. Ideally, we want to be able to define the form model using a JSON structure.

[{label: 'Form entry1', key: 'item1', type: 'input'}, {label: 'form item2',
    key: 'item2',
    type: 'select',
    props: {
      options: []
    }
  }
]
Copy the code

With the above configuration structure, it is expected that the corresponding form template will be generated in the page:

In the above configuration, label represents the form label, type represents the control type corresponding to the form, and key represents the data parameters in the form. Finally, according to the user’s input, we can finally obtain the following data model for submission:

{
  item1: '',
  item2: ''
}
Copy the code

Now that we know the configuration structure of the form, we can start building a form component that just needs to pass in the JSON configuration.

<template> <div> <ConfigForm :formModel="formModel" :formItems="formItems" /> </div> </template> <script> export default {data() {return {formModel: {item1: "}, formItems: [{label: 'item1', key: 'item1', type: 'input'}]}}}Copy the code

Where formModel is the data parameter we need for the final submission, and formItems is a form control for each item.

The component map

First, you can choose your favorite component library to use as the base template for your Form components, in this case using the Form and its related control components from Element-UI as the base.

In our JSON configuration, the Type field is used to represent the different form controls. So we need to maintain a mapping between type and component tag:

const tagMap = {
  input: {
    tag: 'el-input'.props: {
      clearable: true}},select: {
    tag: 'el-select'
  }
  // ...
}
Copy the code

As shown in the code above, a mapping between Type and component is defined. For example, when type is input, the el-Input component of Element is rendered. In addition, we can also configure the component initialization properties in props.

Once we have the component mapping, the next step is to render each component in the form through a V-for loop. Due to the uncertainty of the form control type, we need to use the dynamic component < Component > in Vue.

<el-form-item
  v-for="item in configItems"
  :label="item.label"
  :key="item.key"
>
  <component
    :is="item.tag"
    v-model="formModel[item.key]"
    v-bind="item.props"
  ></component>
</el-form-item>
Copy the code

As shown in the code above, the configItems here are processed from the JSON configuration we passed in.

computed: {
  configItems() {
    return this.formItems.map(item= > formatItem(item, this.formModel)
  }
}
Copy the code

We put the configItems conversion into calculated properties so that the formModel collects configItems’ dependencies, so that the form renders properly in response to the formModel changes.

The core here is the formatItem method, which is the key to form item transformation.

function formatItem(config, form) {
  letitem = { ... config }const type = item.type || 'input'
  const comp = tagMap[type]
  // Map labels
  item.tag = comp.tag
  / / maintenance propsitem.props = { ... comp.props, ... item.props }return item
}
Copy the code

Another problem here is that when we encounter components such as El-Select that are themselves nested, we also need to consider the rendering of the dropdown el-Option. In this case, you need to wrap an additional custom component for the dropdown component, for example:

<form-select :options=""></form-select>
Copy the code

As above, we need to set the options field in the PROPS object in the JSON configuration to ensure that the option value is passed in.

Thus, we can initially build the prototype of the configuration form component. We can configure type and key to complete the corresponding component mapping rendering and data binding, and pass in the original properties of the component or the parameters required by the customized component through the props field.

Linkage between forms

So far we have simply completed the form control according to the type of rendering ability, in the actual development, forms often exist linkage relationship between each item, so we also need to continue to expand the form ability according to the linkage between the form items.

Conditions apply colours to a drawing

That is, when one form item is a specific value, one or more other form items are not displayed (or displayed).

We control the display of form items by adding the ifShow field to the configuration items. Since the presentation of a form item is determined by the dynamic change in the value of another form item, we need to set ifShow to a function and pass formModel as a parameter.

[{label: 'Label1', key: 'item1'}, {label: 'label2', key: 'item2', ifShow(form) { return form.item1 ! == 1}}]Copy the code

As shown in the code above, item2 is displayed only when item1 does not have a value of 1. The formatItem method needs to be modified:

item._ifShow = isFunction(item.ifShow) ? item.ifShow(form) : true
Copy the code

Item._ifShow is passed to the V-if of the el-form-item.

Dynamic range limitation

In some scenarios, if form item 1 is a specific value, form item 2 can only be selected within a fixed range.

[{label: 'Label1', the key: 'item1', type: 'radio', props: [{1: 'radio1, 2:' radio2}]}, {label: 'label2',
    key: 'item2',
    type: 'select',
    ifShow(form) {
      return form.item1 === '1'
    },
    props(form) {
      let options = []
      if (form.item1 === 1) {
        options = { 1: 'select1', 2: 'select2'}
      } else {
        options = { 3: 'select3', 4: 'select4' }
      }
      return {
        options
      }
    }
  }
]
Copy the code

Accordingly, in the formatItem method, determine the type of props and, in the case of functions, pass in the formModel.

const_props = isFunction(item.props) ? item.props(form) : item.props item.props = { ... comp.props, ... _props }Copy the code

As a result, our component template and formatItem method are tuned to:

<el-form-item
  v-for="item in configItems"
  v-if="item._ifShow"
  :label="item.label"
  :key="item.key"
>
  <component
    :is="item.tag"
    v-model="formModel[item.key]"
    v-bind="item.props"
  ></component>
</el-form-item>
Copy the code
function isFunction(fn) {
  return typeof fn === 'function'
}
function formatItem(config, form) {
  letitem = { ... config }const type = item.type || 'input'
  let comp = tagMap[type]
  // Map labels
  item.tag = comp.tag
  / / maintenance props
  const_props = isFunction(item.props) ? item.props(form) : item.props item.props = { ... comp.props, ... _props }// Whether to display
  item._ifShow = isFunction(item.ifShow) ? item.ifShow(form) : true
  return item
}
Copy the code

Form function extension

So far, our form component has been able to render on demand simple linkage between different types of controls and form items. In the actual development work, we also need to make asynchronous data request, call the control component’s own method and so on on the basis of the form. Therefore, we need to continue to extend the functionality of forms.

Component property and method passing

Within Element, form-related components such as Select usually have their own Event API. The dynamic components we use today don’t do a very good job of passing through the API.

The idea of higher-order components is used here, combined with the render method of the Vue function to achieve the purpose. See the render functions & JSX section of the documentation for details.

Before we use the Render function to return the component, let’s take a quick look at the parameters of the createElement method, as shown in the documentation:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // An HTML tag name, component option object, or
  Resolve an async function of any of the above. Required fields.
  'div'.// {Object}
  // A data object corresponding to the attributes in the template. Optional.
  {
    // (see next section for details)
  }
  / / to omit...
)
Copy the code

We mainly use the first two parameters.

The first parameter is that we need to render the component. We can pass in a String that corresponds to our JSON configuration, which is the mapping between type and component tag mentioned above. Therefore, we can pass in item.tag here.

The second parameter is an object, as described on the official website:

{
  // Plain HTML features
  attrs: {
    id: 'foo'
  },
  / / component prop
  props: {
    myProp: 'bar'
  },
  // The event listener is in the 'on' property,
  // Modifiers such as' V-on :keyup.enter 'are no longer supported.
  // keyCode needs to be checked manually in the handler.
  on: {
    click: this.clickHandler
  }
  / / to omit...
}
Copy the code

From this we can see that the Event API that requires passthrough can be merged into the object’s ON field.

In the previous form design, we put the component’s attR property in the props field as well, and here we adjust the configuration to be consistent with that object parameter for ease of observation. In the configuration, we split properties, methods and custom component prop into attrs, ON, and props fields respectively. Attrs and props can be designed as functions or objects so that some properties need to be changed according to the values of other form items.

For example, if we need a searchable Select form item that listens for value changes, we can configure it as follows:

{label: 'Label2',
  key: 'item2',
  type: 'select',
  attrs: {
    filterable: true
  },
  on: {
    change: this.handleChange
  },
  props: {
    options: {
      1: 'select1',
      2: 'select2'
    }
  }
}
Copy the code

Now that we know how to use the Render function, we can try to build a higher-order component to replace the dynamic component:

DynamicCell.vue

<script> export default { props: { item: Object }, render(h) { const { item } = this const WrapComp = item.tag return h(WrapComp, { on: this.$listeners, attrs: {... this.$attrs, ... item.attrs }, props: this.$props }) } } </script>Copy the code

In our form template, we also need to make a few changes:

<el-form-item
  v-for="item in configItems"
  v-if="item._ifShow"
  :label="item.label"
  :key="item.key"
>
  <DynamicCell
    v-model="formModel[item.key]"
    :item="item"
    v-on="item.on" 
    v-bind="item.props"
  ></DynamicCell>
</el-form-item>
Copy the code

Similarly, we need to adjust the formatItem method:

/ / maintenance props
const _props = isFunction(item.props) ? item.props(form) : item.props
item.props = _props
// attrs
const _attrs = isFunction(item.attrs) ? item.attrs(form) : item.attrs
item.attrs = Object.assign({}, comp.attrs || {}, _attrs)
Copy the code

Asynchronous values

In some business scenarios, the Select drop-down box option in a form requires an interface to be invoked to retrieve it asynchronously. At the same time, this data may be used in several different forms in the page, so we need to save the obtained data in Vue’s data. Assuming the variable name is tempOpts, our JSON configuration is as follows:

{label: 'Label2',
  key: 'item2',
  type: 'select',
  props: () => {
    return {
      options: this.tempOpts
    }
    
  }
Copy the code

Here you need to write the props field as an arrow function. In the case of an object, the tempOpts property has not yet been mounted to the instance (this) when data is initialized, so it is undefined. When the props function executes in computed, the initialization of data is complete. Similarly, we need to adjust the formatItem method:

/ / maintenance props
const _props = item.props
  ? isFunction(item.props)
    ? item.props(form)
    : item.props
  : {}
Copy the code

Custom Components

In addition to rendering the form controls provided by Element, some pages of the form have unconventional form elements, and components that appear on this particular page do not need to be set as global components. Therefore, you need to provide the ability to customize component rendering.

We also need to use the render function’s power: we can use JSX syntax in the DynamicCell component we define and render the template using the Render method.

We can add the renderCell field to the JSON configuration, assuming that we currently have a custom component:

{label: 'Label1',
  key: 'item1',
  renderCell: () => {
    return <button-counter prop1={this.prop1} />
  }
}
Copy the code

Similarly, in DynamicCell, renderCell needs to be judged:

render(h) {
  const { item } = this
  const WrapComp = item.tag
  if (item.renderCell) {
    return item.renderCell
  } else {
    return h(WrapComp, {
      on: this.$listeners,
      attrs: {... this.$attrs, ... item.attrs },props: this.$props
    })
  }
Copy the code

Form validation

Form validation is also an integral part of forms. We can directly use the rules property in the el-Form-item component and assign the rules field to it in the JSON configuration.

summary

Through a series of tweaks above, we ended up with a form component with on-demand rendering controls, passthrough apis, render custom components, form validation, and more. In daily project development, it can basically meet the needs.

We end up rendering the template with just one line:

<ConfigForm :formModel="formModel" :formItems="formItems" />
Copy the code

For developers, you only need to write the JSON configuration, so that the linkage and coupling between forms can be centrally reflected in the configuration, making subsequent maintenance clear and easy.

If there are defects, welcome criticism and correction.

reference

  • I don’t want to write forms anymore
  • Explore Vue higher-order components
  • [render function & JXS](