1 Tool Overview

1.1 introduction

Dw-form-making, a Web-based form design tool based on the Element-UI component library.

Project technology stack VUE, VUE-CLI3, visual design of the Form Form composed of element-UI input box, selector, check box and other controls, configuration of Form fields, labels, verification rules and so on.

Earlier versions used VUex. Due to NPM package release and high dependence on VUEX (that is, vuEX needs to be configured after NPM installation), these solutions are abandoned. Vue. Observable is used to realize the state and mutations of VUEX.

The third party components of the project include vueDraggable drag component, Tinymce rich text editor, Clipboard copy plug-in, LoDash function library, ACE code editor, etc. Among them, elder-UI is not included in the NPM release package, minimizing the size of the project and avoiding secondary introduction.

According to the basic version of VUe-form-making, the form components are not rendered by v-IF judgment method. First, there are many form components, almost all of which are V-IF, which is easy to cause code redundancy and poor reading ability. Second, the raster layout adopts component recursion, which leads to poor page rendering performance. V-if repeats several times per recursive page, so discard this method and use dynamic component rendering method, which is not only readable but also high performance.

Due to the frequent use of VUe-form-making, and then more interested in its implementation, on the basis of referring to the original style, the JS part of the project completely broke away from vue-form-making, and rebuilt the basic version code of vue-form-making from scratch.

The project can proficiently consolidate the use of Element-UI form components and partial Dialog boxes, Message prompts, Container layout containers, etc. It involves recursive component scope slot, component circular reference processing, Git multi-remote library maintenance, NPM package distribution.

1.2 Project Preview

Dw – form – making

1.3 sketch

1.4 File and Directory Configuration

├ ─ ─ dist ├ ─ ─ docs ├ ─ ─ lib ├ ─ ─ public ├ ─ ─ the SRC │ ├ ─ ─ assets │ │ ├ ─ ─ fonts │ │ ├ ─ ─ images │ │ ├ ─ ─ js │ ├ ─ ─ components │ │ │ ├── │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├── Bass Exercises. Vue │ ├─ Bass Exercises. Vue │ ├─ Bass Exercises │ │ │ ├ ─ ─ config. Vue │ │ │ ├ ─ ─ the vue │ │ ├ ─ ─ radio │ │ │ ├ ─ ─ config. The vue │ │ │ ├ ─ ─ the vue │ │ ├ ─ ─... │ │ ├ ─ ─ CommonField. Vue │ │ ├ ─ ─ CommonView. Vue │ │ ├ ─ ─ config. Js │ │ ├ ─ ─ index. The js │ │ ├ ─ ─ the js │ ├ ─ ─ layout │ │ ├ ─ ─ Vue │ ├─ │ ├─ Vue │ ├─ Vue │ ├─ Vue │ ├─ vue │ ├─ vue │ ├─ vue │ ├─ vue │ ├─ vue │ ├─ vue │ ├─ vue │ ├─ LinkHeader. Vue │ ├ ─ ─ store │ │ ├ ─ ─ index. The js │ │ ├ ─ ─ vuex. Js │ ├ ─ ─ styles │ │ ├ ─ ─ index. The SCSS │ │ ├ ─ ─ layout. The SCSS │ ├ ─ ─ Utils │ │ ├ ─ ─ index. Js │ │ ├ ─ ─ format. The js │ │ ├ ─ ─ vue - component. Js │ ├ ─ ─ App. Vue │ ├ ─ ─ the main, js │ ├ ─ ─ index. The js │ ├ ─ ─ Package. json │ ├─ Readme.md │ ├─ Vue.config.jsCopy the code

2 the initialization

2.1 Scaffold initialization

Initial empty scaffolding VUE-CLI3 only configure Babel and CSS pre-processors (SCSS), delete the irrelevant parts of other services, and create the folder part step by step as required.

The project core component library, Element-UI, can be directly introduced globally because the entire project is completely dependent on element-UI. However, NPM package distribution is not introduced to minimize the size of the project, which will be discussed later.

npm i element-ui -S

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Copy the code

Secondly, the core of the project drags and drags the business component VueDraggable, and drags and drags the page part into it instead of introducing it globally.

npm i -S vuedraggable

import draggable from 'vuedraggable'.export default {
    components: {
       draggable
    }
...
Copy the code

With normalize.css, the project custom style initializes styles index. SCSS, and the rest of the layouts-related and component-related styles are placed in layout.scss.

2.2 Page Layout

Vue, ViewForm.vue, Widget.vue, The ConfigOption configuration parameter maintains component fieldProperty. vue field properties and formProperty. vue form properties.

The basic layout of the project is determined, and the concrete structure begins to be realized. createlayoutFolders, maintain the relevant parts of the entire page layout,App.vueOnly to dolayoutThe introduction of such lateApp.vueBasically unchanged and, crucially, released asnpmPackage when the wholelayoutRegister as a component for easy import.

// App.vue
<div id="app">
    <Layout />
</div>

import Layout from "./layout/index"; .export default {
  name: "App".components: {
    Layout
  }
}
...

// index.js
import MakingForm from './layout/index'.export{... MakingForm }Copy the code

Layout index.vue is exported as a component, in which layout uses Element-UI container inside the layout container. The four main areas of the page are placed under the same level of components folder, and there is less code at the bottom of the Powered by, so there is no need to pull out. The four main area Settings are named ElementCate with a fixed width of 250px, ConfigOption with a fixed width of 300px, and ButtonView with a minimum width of 440px.

<el-container class="dw-form-making-container">
    <el-header class="dw-form-making-header">
      <link-header />
    </el-header>

    <el-container class="dw-form-making-body">
      <el-aside class="dw-form-making-elementcate" width="250px">
        <element-cate />
      </el-aside>

      <el-main class="dw-form-making-buttonview">
        <button-view />
      </el-main>

      <el-aside class="dw-form-making-configoption" width="300px">
        <config-option />
      </el-aside>
    </el-container>

    <el-footer class="dw-form-making-footer">.</el-footer>
</el-container>
Copy the code

The ElementCate part firstly considers the data and ICONS of each element, ignoring other situations (configuration information, etc.). Iconfont creates a personal project, selects the appropriate icon, downloads the local compression package, decompress the import, and pays attention to adding the ~ symbol before the iconfont. Query the related path from the alias in vue.config.js to load the module.

@import "~assets/fonts/iconfont.css"
Copy the code

Three different categories of form components are introduced in ElementCate. Vue. Suppose that a JS file (elements folder index.js) exports three arrays, namely basic, advance and layout, and each array object temporarily contains the name tag and icon icon.

// element -> index.js
const basic = [
    ...
    {
        name: "One line of text".icon: "icon-input"}... ]const advance = []

const layout = []

export {
   basic,
   advance,
   layout
}
Copy the code

ElementCate. Vue introduces three arrays, temporarily rendered using UL Li, set to block level and specify a width of 48%, where ICONS and component names are aligned with the center line, and set the style for the form components to float.

The ButtonView view area is divided into two parts: the button area and the view area. The corresponding button is temporarily placed in the button area, and the event follow-up access logic is processed in detail. The view area is removed into the component ViewForm.vue, and now a DIV box is temporarily placed.

The ConfigOption configuration parameter area is divided into field properties and form properties. The basic Tabs switch can be implemented.

2.3 vuedraggable Drag and transition-group

The vuedraggable official documentation provides an example method of using vuedraggable with transition-group, which details the item element classification and configuration parameters for the view form area.

  • tag: draggableThe rendered tag name
  • value: and internal elementsv-forThe directive references the same array and should not be used directly, but can be passedv-model
  • group.name: Same group name can be dragged together, differentdraggableYou can do lists
  • group.pull: Drag to another group to clone or copy, rather than remove and move
  • group.put: Drag other groups to check whether the current group is added
  • sortDrag in the same group without sorting
  • animationUnits:ms, andtransition-groupHave a transitional effect
  • ghostClass: Dragged elementclassThe name of the class
  • handle: Drag the part of the list element that specifies the class name (drag the small icon) to drag
  • clone: Clone event, declared use:, processing the cloned elements
  • add: Add event, drag other groups to the current group, process the element before adding
// ElementCate.vue
<draggable
   tag="ul"
   v-model="list"
   v-bind="{ group: { name: 'view', pull: 'clone', put: false }, sort: false }"
   :clone="handleClone"
>
    <li>.</li>. </draggable>// ViewForm.vue
<draggable
    v-model="list"
    v-bind="{ group: 'view', animation: 200, ghostClass: 'move', handle: '.drag-icon', }"
    @add="handleAdd"
>
   <transition-group>.</transition-group>
</draggable>
Copy the code

According to the above configuration, the ElementCate part introduces the classification list basic, advance and layout, and registers the Draggable component. If the length of the classification list is 0, the corresponding list title is not displayed, and DOM elements are not added in the outer layer. Template and V-IF are used. The clone function is triggered when it drags, taking the copied element object as an argument, temporarily printing, and returning the copied object.

The absolute position is used in the view form to make the height of the whole lower part of the region. If the height of the draggable area is not high enough, it will not drag and the list bound internally is temporarily a variable in data. The add function argument deconstructs the newIndex (in-list index) by which the dragged elements are retrieved. The console views the list in the view form. When the element is dragged in (without releasing the mouse), the element class is named elemental-Cate -item Move. When the mouse is released, render as the view form list element, and layout.scss sets the drag style. Because view forms also have dragging elements, styles are declared as variables and introduced when used.

@mixin form-item-move {
    outline-width: 0; height: 3px; overflow: hidden; border: 2px solid #409eff; . } .element-cate-item { &.move{ @include form-item-move; }... }Copy the code

FormProperty can set alignment, width, component size, etc., in the button view, so put dragGable in the button view into the El-Form component. Each list element is rendered as el-Form-Item, and the el-Form configuration is fixed. El-form-item temporarily renders the label and input box. Note that the transition-group inner element must have a key value, otherwise the element will not render and the console will print a warning.

<el-form 
  size='small'
  label-width='100px'
  label-position='right'
>
    <draggable . @add='handleAdd'>
        <transition-group>
            <div 
               class='view-form-item' 
              v-for='(element, index) in data' 
              :key='index'
            >
                <el-form-item :label='element.name'>
                    <el-input />
                </el-form-item>
            </div>
        </transition-group>
    </draggable>
</el-form>

handleAdd({ newIndex }) {
  this.select = this.data[newIndex]
}
Copy the code

Drag the ElementCate element into the ViewForm to see the blue bar, release the mouse to render the input box and label, set the View-form-Item style and hover style, and the border color is the same as the ElementCate element. When view-form-item is clicked, the data variable select saves the clicked view-form-item and determines that the blue border and drag icon are displayed.

<div
    :class="[ 'view-form-item', { active: select.key === element.key }, ]"
    @click="handleSelect(element)". ><el-form-item .>.</el-form-item>. <divclass='item-drag' v-if="select.key === element.key">
      <i class="iconfont icon-drag drag-icon"></i>
    </div>
   ...
</div>

handleSelect(element) {
  this.select = element
}
Copy the code

The first thing to be clear is that the clone event returns the same object as the view form, so you can add the key attribute in the Clone callback or the newIndex element in the form view add event. However, there are obvious differences between the two methods. In the former case, drag clone to return the object and add the key value; release the mouse to add the active element select as the current element (the dragged element is highlighted); in the latter case, drag Clone to return the object and release the mouse to add the key and then set the active element. Although the implementation is the same, the latter function does two things (add key, highlight) and does not comply with the single responsibility principle SRP.

The key value uses a random 4-bit string and timestamp. The clone function takes a drag element reference, so you must make a copy of the object when you return it. Lodash. deepClone is used for object copy, and you can also use JSON deep copy. Uuid and deepClone are exposed under utils.

// ElementCate.vue
handleClone(element) {
      return Object.assign(deepClone(element), { key: uuid() })
}

// utils -> index.js
import lodash from 'lodash';

function deepClone(object) {
    return lodash.cloneDeep(object);
}

function S4() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}

function uuid() {
    return Date.now() + '_' + S4()
}

export {
    uuid,
    deepClone
    ...
}
Copy the code

2.4 Elements parameters and VUEX

The above part has basically implemented element drag and click highlighting, but the View-form-item is also rendered as an input box. If ElementCate elements have configuration parameters, different form elements can be rendered according to different configurations, temporarily using V-IF mode.

// elements -> index.js
const basic = [
    {
        name: 'Single line text'.icon: 'icon-input'.type: 'input'
    },
    {
        name: 'Multiline text'.icon: 'icon-textarea'.type: 'textarea'}... ]// ViewForm.vue
<el-form-item :label='element.name'>
   <el-input v-if='element.type === "input"' />
   <el-input type='textarea' v-if='element.type === "textarea"' />. </el-form-item>Copy the code

ElementCate elements drag in, highlight, and field attributes can be configured with different parameters, but field attributes are not associated with the view form. The basic version of VUe-form-making uses component pass values internally. The active element SELECT is passed to the top-level layout and sent to FieldProperty.vue. The component hierarchy is deep and the code is not readable. If there is global state management, the solution is flexible without affecting the component hierarchy and structure.

Vuex is a good solution to these problems, but the project is not dependent on VUex and the project is not large enough to just use state management. The Vue. Observable approach not only implements some vuex functions, but also makes the project seem lightweight. The select active element in the view form is maintained under state, computed is introduced in the view form, and mutations are invoked when elements are dragged in and clicked. Fieldproperty. vue also introduces select, temporarily configurable element label names.

// store -> index.js
 export default new Vuex.Store({
    state: {
        select: {}},mutations: {
        SET_SELECT(state, select) {
            if (state.select === select) return
            state.select = select
   }
}

// ViewForm.vue
import store from 'store/index.js'

export default{...computed: {
    select() {
      returnstore.state.select; }},methods: {
      handleSelect(element) {
          store.commit("SET_SELECT", element);
      },
      handleAdd({ newIndex }) {
          store.commit("SET_SELECT".this.data.list[newIndex]); }}},// FieldProperty.vue
<el-form size="small" label-position="top">
   <el-form-item label="Label">
       <el-input v-model="data.name"></el-input>
   </el-form-item>
</el-form>

export default{...computed: {
    data() {
      returnstore.state.select; }}}Copy the code

In fieldProperty. vue, the render configuration will be different if the single-line text contains placeholder and the multi-line text does not contain placeholder. Placeholder, unlike the name tag, belongs to the element configuration and is placed under options.

// elements -> index.js
const basic = [
    {
        name: 'Single line text'.icon: 'icon-input'.type: 'input'.options: {placeholder:' '}}, {name: 'Multiline text'.icon: 'icon-textarea'.type: 'textarea'}... ]// ViewForm.vue
<el-form-item :label='element.name'>
   <el-input 
       :placeholder='element.options.placeholder' 
       v-if='element.type === "input"' 
   />
   <el-input type='textarea' v-if='element.type === "textarea"' />. </el-form-item>// FieldProperty.vue
<el-form size="small" label-position="top">
   <el-form-item label="Label">
       <el-input v-model="data.name"></el-input>
   </el-form-item>
   <el-form-item v-if='data.type === "input"' label="Placeholder content">
       <el-input v-model="data.options.placeholder"></el-input>
   </el-form-item>
</el-form>
Copy the code

3 Form element operations

3.1 Global form configuration

The form is also a global variable that contains the form configuration (alignment, width, component size, and so on) and internal elements. ViewForm. Vue internal data is maintained to store, and the operation of form and active elements is basically inside mutations.

// store -> index.js
export default new Vuex.Store({
    state: {select: {},
        data: {list: [].config: {
                labelWidth: 100.labelPosition: "right".size: "small".customClass: ' ',}}}})// ViewForm.vue
<el-form
      :size="data.config.size"
      :label-width="data.config.labelWidth + 'px'"
      :label-position="data.config.labelPosition">... </el-form>export default {
  computed: {
    data() {
      return store.state.data
    }
  }
}

// FormProperty.vue
<el-form label-position="top" size="small">
      <el-form-item label="Label Alignment">
        <el-radio-group v-model="data.labelPosition">.</el-radio-group>
      </el-form-item>
</el-form>

export default{...computed: {
    data() {
      return store.state.data.config
    }
  }
}
Copy the code

3.2 Dynamic Components

Currently elements can be dragged to view forms and configured with labels, etc. Forms can also be configured globally. But button to view element is a single, gradually perfect the quantity as many as 20 or so after, if the input box and other components through the v – if only render judgment, first is v – almost as a whole if congruent judgment, reading is very bad, the second for each component rendering can v – if, after 20 times view form lattice are introduced to each nested grid level, If the V-if is repeated 20 times, the rendering performance of the form will be very poor once the raster level is deep and there are many elements. Moreover, when the form components are added by custom in the later stage, there will be many places to adjust the code every time a component is added, which is very difficult to maintain. Refer to the vue-form-making base version, the advanced version may have been refactored and perform well. The same goes for form configuration. The full V-IF is not the ultimate solution, and dynamic components will do just fine.

ElementCate each element corresponds to a form component and form configuration component. Dynamic rendering according to the component name of ElementCate will greatly simplify the code amount, but view form initialization and field attribute initialization need to introduce multiple components, requiring automatic import module requiring requiring. Context. Avoid duplicate code and manual imports.

Elements places form components, ElementCate adds components, and adds new components under Element without considering rendering inside the view form. Index.js configures ElementCate elements, view.js and config.js automatically import config.vue and register components.

Add a single-line text component, create a new input under Elements, create config.vue and view.vue, and you must configure the component name.

// elements component directory│ ├ ─ ─ elements │ │ ├ ─ ─ input │ │ │ ├ ─ ─ config. The vue │ │ │ ├ ─ ─ the vue │ │ ├ ─ ─... │ │ ├ ─ ─ config. Js │ │ ├ ─ ─ index. The js │ │ ├ ─ ─ the js// index.js
const basic = [
    {
        name:'Single line text'.icon: 'icon-input'.type: 'input'.component: 'DwInput'.options: {placeholder:' '}}]// view.vue<el-form-item ... ><el-input
       :placeholder="element.options.placeholder"
       .
    ></el-input>
</el-form-item>

export default {
  name: "DwInput"
  props: {element: {
          type: Object,}}... }// config.vue
<el-form size="small" label-position="top">
    <el-form-item label="Label">
       <el-input v-model="data.name"></el-input>
    </el-form-item>
</el-form>

export default {
  name: "DwInputConfig". }Copy the code

View. js dynamically imports all view.vue files under Elements and exports them as component lists, the same as config.js.

// view.js
const components = {}

const requireComponent = require.context("elements/".true./(view.vue)$/);

requireComponent.keys().forEach(fileName= > {
    const componentOptions = requireComponent(fileName);

    const component = componentOptions.default || componentOptions;

   components[component.name] = component
});

export default components

// ViewForm.vue
<div class='view-form-item' v-for='element in data.list'. ><component :is='item.component' :element='element'>
    
    <div class='item-drag' v-if="select.key === element.key">
         <i class="iconfont icon-drag drag-icon"></i>
    </div>
</div>

import Draggable from "vuedraggable"
import components from "elements/view"

export default{...components: { Draggable, ... components } }// FormProperty.vue
<component
    :is="component.component && `${component.component}Config`"
    :data="component"
/>

import components from "elements/config"

export default {
  components,
  computed: {
    component() {
      return store.state.select
    }
  }
}
Copy the code

3.3 Public Field Properties and Public Views

General field attributes include field identifier model, label name, labelWidth (isLabelWidth, labelWidth), hidden label hideLabel and customClass customClass. Field identifier is the field name. The default generated field identifier is the element type plus the key value, and the field identifier Model is generated along with the key value.

// ElementCate
handleClone(element) {
      const key = uuid();
      return Object.assign(deepClone(element), {
        key,
        model: element.type + "_" + key
      })
}
Copy the code

The five properties are encapsulated as the common component CommonField.vue, which places the slot, the component config.vue reference, and the component’s unique configuration is inserted into the slot. Note that component-passed values are one-way, but commonField. vue internally modifies incoming values. The reason is that component-passed values actually pass the reference address, so changes inside the component are synchronized externally. Component values can be bidirectional not only by using Sync, but also by passing reference types.

<common-field :data="data">
    <template slot="custom">.</template>
</common-field>

import CommonField from ".. /CommonField";

export default {
  components: {
    CommonField
  }
}
Copy the code

Commonfield. vue synchronizes with Commonfield. vue, el-form-item slot label and label-width explicit label, el-form-item adds class custom customClass, IsLabelWidth Controls the label width, the label is not displayed, the width is 0, the label is displayed with a custom width, the width is a custom value, the label is displayed with no custom width, the width is the form label width.

<el-form-item
    :label-width=" element.options.hideLabel ? '0px' : (element.options.isLabelWidth ? element.options.labelWidth : config.labelWidth) + 'px' "
    :class="element.options.customClass"
>
  <template slot="label" v-if=! "" element.options.hideLabel">
      {{ element.name }}
  </template>

  <slot></slot>
</el-form-item>
Copy the code

3.4 Element copy, deletion and pseudo element

The field identifier of the element in the ViewForm can be used to create div boxes or CSS pseudo-elements.

View-form-item Custom attribute data-model, CSS pseudo-element Content internal attR function to obtain. ViewForm elements are forbidden to be entered. Similarly, you can locate div boxes or CSS pseudo-elements. The absolute positioning of pseudo-elements is set to 0 and the parent element is positioned relative to the parent element.

// ViewForm.vue
<div 
    class='view-form-item'
    v-for='element in data.list'
    :data-model='element.model'>... </div>// layout.scss
.view-form-item{
    position: relative; . &::before {content: attr(data-model);
      position: absolute;
      top: 3px;
      right: 3px;
      font-size: 12px;
      color: rgb(2.171.181);
      z-index: 5;
      font-weight: 500;
    }
    &::after {
      position: absolute;
      content: "";
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      z-index: 5; }}Copy the code

Add clone icon inside view-form-item, pass parameters include clone element, index value, list element, handle function maintenance in store, copy element key value and model to regenerate, clone active element select reset.

// ViewForm.vue
<div class='view-form-item'
    v-for='(element, index) in data.list'>... <iclass="iconfont icon-clone"
       @click="handleClone(element, index, data.list)"
    />
</div>

handleClone(element, index, list) {
      store.commit("CLONE_ELEMENT", { index, element, list })
}

// store -> index.js
CLONE_ELEMENT(state, { index, element, list }) {
    const key = uuid();
    const el = deepClone(element);
    
    list.splice(index + 1.0.Object.assign(el, {
       key,
       model: element.type + "_" + key,
    }))

    state.select = list[index + 1]}Copy the code

To avoid repeated clicking, the delete button is triggered only when it is clicked for the first time, and cannot be clicked again during the triggering process of the element deletion animation. Update the active element select before deleting the element. The deleted element is at the end of the list and has a length greater than 1. The active element is the previous element. If it is at the end of the list and the length is equal to 1, the list has only one element and the active element is empty. If the above is not met, the element is in the middle, and the active element is the next element after deletion.

// ViewForm.vue
<div class='view-form-item'>... <iclass="iconfont icon-trash"
       @click.once="handleDelete(data.list, index)"
    />
</div>

handleDelete(list, index) {
      store.commit("DELETE_ELEMENT", { list, index });
},

// store -> index.js
DELETE_ELEMENT(state, { list, index }) {
    if (list.length - 1 === index) {
        state.select = index ? list[index - 1] : {}}else {
        state.select = list[index + 1]
    }
    list.splice(index, 1)}Copy the code

4 ElementCate components

Component parameters refer to commonFild. vue public field attributes and contain five public attributes by default. Width, operation attributes, and verification rules are added as required. Custom attributes are added to components and inserted into components by slots. The component view refers to the commonView. vue public view, which is responsible for the form activity style, label, field properties, etc. After the component reference, it does not consider the form presentation, but only focuses on the component parameter part.

The component view part view.vue, since the form preview can obtain the internal value of the form, it is obvious that the component realizes v-Model two-way binding, and the component temporarily receives the value passed inside, and then customize the component V-Model in the preview part.

Placeholder, Style width, and so on are the reference sources for the component customization.

4.1 Single-line text

Single-line text parameters include width, default value, placeholder content, operation attributes, etc. The verification rules are complex and are not considered for the time being. New component parameters and view sections can be referenced in the single-line text source code. Single-line text is either disabled or read-only, and cannot be applied to the same form element at the same time.

// elements -> input -> config.vue
<template slot="option">
  <el-checkbox
     v-model="data.options.disabled"
     :disabled="data.options.readonly"
  >disable</el-checkbox>
  <el-checkbox
     v-model="data.options.readonly"
     :disabled="data.options.disabled"
 >read-only</el-checkbox>. </template>Copy the code

4.2 Multi-line text

Multi-line text parameter section, default using text field.

// elements -> textarea -> config.vue
<el-form-item label="Default value">
  <el-input type="textarea" . />
</el-form-item>
Copy the code

4.3 the counter

Counter operation button position pass parameter controls-position, default is default, default value is limited by maximum, minimum, number of steps.

// elements -> number -> view.vue
<common-view>
    <el-input-number
      :value='element.options.defaultValue'
      :controls-position="element.options.controlsPosition"
    />
</common-view>

// elements -> number -> config.vue
<el-form-item label="Default value">
   <el-input-number
     :max="data.options.max"
     :min="data.options.min"
     :step="data.options.step"
     v-model="data.options.defaultValue"
   />
</el-form-item>
Copy the code

4.4 Single Click Group

Single box group layout is divided into block-level and in-line. Options include static data and dynamic data, and dynamic data is not considered. Options are label-value pairs. When you clear the list, the default value is cleared. If the selected item is deleted, the default value is cleared. Note that the EL-Radio component, if label is not displayed, can pass in a null double parenthesis value.

// elements -> radio -> view.vue
<el-radio
    v-for="(item, index) in element.options.options"
    style="{ display: element.options.inline ? 'inline-block' : 'block' }"
>{{ item.label }}</el-radio>

// elements -> radio -> config.vue
<li .>
   <el-radio :label="item.value">{{}}</el-radio>.</li>
Copy the code

4.5 Multiple Box Groups

The default value of the multi-box group is array. You can select multiple default values of the multi-box group. When deleting a selected item, the index of the value of the selected item in the default value is obtained.

// elements -> checkbox -> config.vue
handleDeleteOptions(element, index) {
   var i = this.data.options.defaultValue.indexOf(element.value);
   if (i > -1) {
      this.data.options.defaultValue.splice(i, 1);
   }
   this.data.options.options.splice(index, 1);
}
Copy the code

4.6 Time selector

The time picker defaults to a format controlled value that can be disabled or read-only, as with a single line of text. Time selection is placeholder content, and range selection includes start placeholder content, range separator, and end placeholder content. The default value can only be NULL when range selection is enabled, and null when range selection is disabled. The el-time-picker component v-bind is bound to is-range, which is a bug of element itself. V-if is consistent with range selection parameters and can be solved by specifying a key value.

// elements -> time -> config.vue
<el-form-item label="Default value">
   <el-time-picker
     key="range"
     v-if="data.options.isRange"
     is-range
     .
   />
   <el-time-picker
    key="default"
    v-else
    .
  />
</el-form-item>

handleRangeChange() {
   this.data.options.defaultValue = this.data.options.isRange ? null : "";
}
Copy the code

4.7 Date picker

Date selector display types include month, year, date, multi-date, date range, etc. The default value of the range type is NULL, and other values are blank characters. The format of the range type is corresponding.

// elements -> date -> config.vue
export default {
  data() {
    return {
      type: [{...label: "Date and time range".value: "datetimerange".format: "yyyy-MM-dd HH:mm:ss".type: null.isRange: true,}},methods: {
    handleTypeChange(value) {
      const showType = this.type.find(e= > e.value === value);
      this.data.options.format = showType.format;
      this.data.options.defaultValue = showType.type; . }}}Copy the code

4.8 score

The default value of the score is controlled by semi-selection and the maximum value. The minimum value is 1, and the default value is cleared to 0.

// elements -> rate -> config.vue
<el-rate
  ...
  :allow-half="data.options.isAllowhalf"
  :max="data.options.max"
/>
Copy the code

4.9 Color pickers

After selecting the color, the element defaults to hex. Check transparency and click on the color picker. The default color does not change.

// elements -> color -> config.vue
<el-form-item label="Default value">
  <el-color-picker
    key="alpha"
    v-if="data.options.showAlpha"
    .
    show-alpha
  />
  <el-color-picker
    key="default"
    v-else
    .
 />
</el-form-item>
Copy the code

4.10 Drop-down selector

Adding options in a drop-down selector is the same as adding options in a single – box group. Deleting elements is the combination of single – box groups and multiple – box groups. Radio transition to multiple, radio does not select the default value, the value is empty array, radio select the default value, the value is the array containing the default value. Multiple select transition single, multiple select default value, value is null, multiple select default value, value is the first element of the array.

// elements -> select -> config.vue
handleMultipleChange(multiple) {
      var value = this.data.options.defaultValue;
      this.data.options.defaultValue = multiple
        ? value === null ? [] : [value]
        : value.length ? value[0] : null;
}
Copy the code

4.11 switch

Switch By referring to the el-switch parameter, you can customize the enabled or disabled text color and text description.

// elements -> switch -> view.vue
<el-switch
 :active-color="element.options.isColor ? element.options.activeColor : '#409EFF'"
 :inactive-color="element.options.isColor ? element.options.inactiveColor : '#C0CCDA'"
 :active-text="element.options.isText ? element.options.activeText : ''"
 :inactive-text="element.options.isText ? element.options.inactiveText : ''"
/>
Copy the code

4.12 the slider

Default slider values are limited by maximum, minimum, and step size.

// elements -> slider -> config.vue
<el-slider
  :max="data.options.max"
  :min="data.options.min"
  :step="data.options.step"
  v-model="data.options.defaultValue"
/>
Copy the code

4.13 words

Text is just a short paragraph that enriches the description of a list of components and parts of a form, and since widths can be specified, the element is block-level.

// elements -> text -> view.vue
<div :style="{ width: element.options.width }">
  <span style='word-break: break-all; '>{{ value }}</span>
</div>
Copy the code

4.14 HTML

The default value of the HTML component is temporarily a text field, which can be filled in with HTML code. The view part uses v-HTML instructions.

// elements -> html -> view.vue
<div :style="{ width: element.options.width }">
  <div v-html="value" />
</div>
Copy the code

4.15 Cascading selector

The cascading selector usually gets the data source asynchronously. By default, it contains the label, value, and children fields. It can also specify the property configuration.

// elements -> cascader -> view.vue
<el-cascader
  :props="{ value: element.options.props.value, label: element.options.props.label, children: element.options.props.children, }"
  :options="[]". />Copy the code

4.16 line

Content-position Controls the position of the text.

// elements -> divider -> view.vue
<el-divider :content-position="element.options.textPosition">
    {{ element.name }}
</el-divider>
Copy the code

5 grid layout

The above section only supports single-line, single-form components, not a simple raster layout where multiple form components cannot be displayed on a single line. The base version of VUe-form-making does not support raster layouts, but its styles and parameters can be used as a reference.

The raster style is different from other components. View-form-item determines whether it is a raster element and dynamically generates the class name. The raster style weight should be higher than the normal style, and the raster style code sequence is stacked after the normal style.

// ViewForm.vue
<div
  :class="[ 'view-form-item', { active: select.key === element.key, grid: element.type === 'grid', }, ]". >... </div>// layout.scss
.view-form-item{
    ...
}

.view-form-item.grid{
    ...
}
Copy the code

The grid parameter is not considered for the time being, showing two columns in one row. In contrast to viewForm. vue, if elements in ElementCate can be dragged into the grid, first of all, the list rendered in the grid must be bound to the Draggable, that is, the Draggleble contains the raster list, and second, the draggable coverage area must be high enough, otherwise elements cannot be dragged in. The element object is temporarily rendered in the raster, and the variable in the data is bound to the V-model. After the element is dragged in, the data can be observed and rendered.

// elements -> grid -> view.vue
<el-row type="flex">
    <el-col :span="12">
      <draggable
        v-model="list"
        v-bind="{ group: 'view' }"
      >
        <transition-group tag="div" class="el-col-list">
          <div v-for="(element, index) in list" :key="index">
            <span>{{ element }}</span>
          </div>
        </transition-group>
      </draggable>
    </el-col>
    <el-col :span="12">
        <div class="el-col-list"></div>
    </el-col>
</el-row>

...
export default{...data(){
        return {
            list: []}}}Copy the code

Elements in el-col-list are rendered as form components, partial batch registration components.

// elements -> grid -> view.vue
<transition-group tag="div" class="el-col-list">
    <component
       v-for="(element, index) in list" :key="index"
       :is="element.component"
       :element="element"
    />
</transition-group>

import Draggable from "vuedraggable";
import components from "elements/view";

export default{...name: "DwGrid".components: { Draggable, ... components },data(){
    return {
      list: []}}}Copy the code

When the ElementCate element is dragged in, the console will report an error that the component is not registered, but the component is explicitly registered in the code. Print this. within the life cycle beforeCreate print this. New Options.com ponents, the page registers only Draggable and grid DwGrid components. Other components registered in batches do not exist, that is, components are not registered. The error is caused by circular references between components that would not exist if form elements were registered globally. But component local registrations, references to DwGrid within DwGrid, become a loop, and components don’t know how to fully resolve themselves. Webpack’s asynchronous import is not applicable to batch registrations. De-register it during the life cycle beforeCreate.

import components from "elements/view";

export default{...beforeCreate() {
     Object.assign(this.$options.components, components); }}Copy the code

At this time, ElementCate elements are dragged in and corresponding forms can be rendered. Refer to viewForm-list inside viewForm. vue and configure draggable parameters.

// elements -> grid -> view.vue
<transition-group class="el-col-list". ><div
      v-for="element in list"
      :key="element.key"
      :class="[ 'view-form-item', { active: select.key === element.key, grid: element.type === 'grid', }, ]"
     :data-model="element.model"
     @click.stop="handleSelect(element)"
   >
       <component
           :is="element.component"
           :element="element"
       />

       <div class="item-drag" v-if="select.key === element.key">
           <i class="iconfont icon-drag drag-icon"></i>
       </div>

       <div class="item-action" v-if="select.key === element.key">
           <i
             class="iconfont icon-clone"
             @click.stop="handleClone"
           ></i>
           <i
             class="iconfont icon-trash"
             @click.stop.once="handleDelete"
           ></i>
       </div>
    </div>
</transition-group>

export default {
  methods: {
    handleSelect(element) {
      store.commit("SET_SELECT", element);
    },

    handleClone() {},

    handleDelete(){}}}Copy the code

Viewform. vue is exactly the same as the view-Form-item code inside the raster (the logic part is not considered for the moment). The common code is usually removed and packaged into a component, but the page structure can be sorted out and finally found that the code consistency is inevitable. First of all, viewForm. vue is a single list. Components drag and render individual elements. After introducing grids, each grid represents a list. So the code in the final list (view-form, el-col-list) is the same.

The common part of the code is the Widget.vue Widget, which is a layer of wrapping for each component, including click highlight, drag, clone, delete events, and component values temporarily element, index.

// ViewFrom.vue
<transition-group class="view-form">
    <widget
       v-for="(element, index) in data.list"
       :index='index'
       :key="element.key"
       :element="element"
    />
</transition-group>

import Widget from "./Widget";

export default {
  components: {
    Widget
  }
}
Copy the code

The raster component introduces widgets inside, drags in ElementCate elements, and the page reports an error component rendering failure. According to the error information, it is difficult to find the cause of the problem, and the page structure is carefully sorted out. Widgets are introduced into form components in batches, including input boxes, grids, etc. The grids are introduced into widgets, which are cyclic references of components. Since it is a single component, the asynchronous import of Webpack is adopted.

// elements -> grid -> view.vue
<transition-group class="el-col-list">
    <widget
       v-for="(element, index) in list"
       :index='index'
       :key="element.key"
       :element="element"
    />
</transition-group>

// import Widget from "components/ButtonView/Widget.vue"

export default {
  components: {
    Widget: () = > import("components/ButtonView/Widget.vue")}}Copy the code

The number of raster columns is not bound to raster JSON data. The form elements in the raster list are part of raster JSON. The columns array holds raster objects.

// elements -> index.js
const layout = [
  {
     ...
     type: "grid".name: "Grid layout".columns: [{list: []}... ] }]// elements -> grid -> view.vue
<el-row>
    <el-col 
      :span="12" 
      v-for="(column, index) in element.columns"
      :key="index"
    >
      <draggable v-model="column.list" .>.<widget
             v-for="(element, index) in column.list"
             :index='index'
             :key="element.key"
             :element="element"
          />
      </draggable>    
    </el-col>
</el-row>
Copy the code

Grid parameters can be configured in horizontal and vertical arrangement mode, including Flex and response mode. The default grid mode is Flex. Refer to Layout for detailed description of parameters.

// elements -> index.js
const layout = [
  {
     ...
     type: "grid".name: "Grid layout".options: {
        gutter: 0.isFlex: true.justify: "start".align: "top",},columns: [{span: 12.xs: 12.sm: 12.md: 12.lg: 12.xl: 12.list: []}... ] }]// elements -> grid -> view.vue
<el-row
    type="flex"
    :gutter="element.options.gutter"
    :justify="element.options.justify"
    :align="element.options.align"
  >
    <el-col
      :xs="element.options.isFlex ? undefined : column.xs"
      :sm="element.options.isFlex ? undefined : column.sm"
      :md="element.options.isFlex ? undefined : column.md"
      :lg="element.options.isFlex ? undefined : column.lg"
      :xl="element.options.isFlex ? undefined : column.xl"
      :span="column.span"
      .
    >.</el-col>
</el-row>
Copy the code

Drag the raster elements to highlight, like in viewForm. vue, to find the elements by index.

// elements -> grid -> view.vue<el-row ... ><el-col .>
        <draggable @add="handleAdd($event, column)" .>.</draggable>
    </el-col>
</el-row>

handleAdd({ newIndex }, column) {
   store.commit("SET_SELECT", column.list[newIndex]);
}
Copy the code

Similar to the original viewForm. vue, delete element pass parameters include index values, element lists, and widget. vue declare component pass values of data, as well as raster.

// ViewForm.vue
<widget
   v-for="(element, index) in data.list"
   :index='index'
   :data='data'
   :key="element.key"
   :element="element"
/>

// elements -> grid -> view.vue
<widget
   v-for="(element, index) in column.list"
   :index="index"
   :data='column'
   :key="element.key"
   :element="element"
/>
Copy the code

Intra-widget cloning is similar to viewForm. vue. If the grid is nested in multiple layers or contains other form components, not only copies are generated after cloning, but the keys of all elements in the copy cannot be the same as before, and the keys of the elements need to be updated recursively.

// store -> index.js
CLONE_ELEMENT(state, { index, element, list }){...if (el.type === "grid") { resetGridKey(el); }...function resetGridKey(element) {
       element.columns.forEach((column) = > {
          column.list.forEach((el) = > {
              var key = uuid();
              el.key = key;
              el.model = el.type + "_" + key;
              if (el.type === "grid") { resetGridKey(el); }}); }); }}Copy the code

6 Dialog Public Dialog box and AceEditor

Import JSON, paste JSON data to quickly configure the form, click OK, render the form according to the configured JSON data, but the data is not limited, a lot of errors occur, utils format. Js verify the incoming JSON data format is correct, reject the format is incorrect and return the error cause. Update the form data data in state with the correct format.

// layout -> components -> ButtonView.vue
handleUploadJson() {
      formatJson(this.$refs.uploadAceEditor.getValue())
        .then((json) = > {
          store.commit("SET_DATA", json);
          this.showUpload = false;
        })
        .catch((err) = > {
          this.$message({
            message: "Wrong data format".type: "error".center: true});console.error(err);
        });
}
Copy the code

To paste JSON data, the asynchronous clipboard reading method MDN can only be used if the user has previously granted the site or application permission to access the clipboard. Use navigator.clipboard to access the clipboard, and readText() reads the clipboard contents asynchronously. Due to security concerns, the clipboard contents cannot be read by browsers that are not local or by websites that use HTTP protocol. You can print navigator.clipboard on the console of HTTP or HTTPS websites. Therefore, you can paste it only when the HTTPS website or the user grants it. Otherwise, the cancel button is displayed and the user manually pastes it.

// layout -> components -> ButtonView.vue. <template slot="action">
   <el-button size="small" v-if="showPasteBtn" @click="handlePaste"
   >paste</el-button>
   <el-button size="small" v-else @click="showUpload = false"
   >cancel</el-button>
</template>

...
export default {
    data(){
        return {
            showPasteBtn:!!!!! navigator.clipboard } },methods: {...handlePaste() {
          navigator.clipboard.readText().then((res) = > {
            this.$refs.uploadAceEditor.setValue(res); }); }}Copy the code

The active element SELECT is cleared and the list in the view is cleared. Generate JSON, that is, display the form’S JSON information. The replication function introduces the third-party replication plug-in Clipboard. The clipboard instance parameters are the name of the button class, the copy content, and the secondary encapsulation prompt message. The clipboard instance is destroyed after replication.

// layout -> components -> ButtonView.vue
<el-button ... class="copyJson" @click="handleCopyJson"</el-button >handleCopyJson() {
  this.handleCopyText("jsonAceEditor".".copyJson");
}

handleCopyText(ref, className) {
   copyText(this.$refs[ref].getValue(), className)
     .then((res) = > {
       this.$message({
          message: "Copy successful".type: "success".center: true}); }) .catch((err) = > {
       this.$message({
          message: "Replication failed".type: "error".center: true}); })}// utils -> index.js
function copyText(text, className) {...var clipboard = new Clipboard(className, {
        text: () = > text
    })
    return new Promise((resolve, reject) = > {
        clipboard.on('success'.() = > {
            resolve()
            clipboard.destroy()
        })
        clipboard.on('error'.() = > {
            reject()
            clipboard.destroy()
        })
    })
}
Copy the code

The purpose of the design tool is to design JSON form data, which is passed by a separate component and rendered as a form. Encapsulate the independent component GenerateForm.vue, buttonView. vue imports and inserts the preview popbox slot, passing in data in global state.

// layout -> components -> ButtonView.vue
<public-dialog>
    ...
    <generate-form :data="data" />
</public-dialog>

...
export default {
    computed(){
        data(){
            return store.state.data
        }
    }
}

// components -> ButtonView -> GenerateForm.vue
<div class="generate-form">
    <el-form .>
      <component ./>
    </el-form>
</div>

export default {
    props: {data: {... }}}Copy the code

Click Preview, the single component is normally rendered (text and HTML are not displayed), and the residual ICONS of the single component are rendered in the grid. The reason is because of the widgets that are introduced inside the raster component, and the ICONS inside the widget, which should not be rendered inside the raster when previewing, but rather the form elements. Generateform. vue imports components in batches, components pass values that cannot be dragged, grids receive parameters and are rendered only as form elements. The key for each component is added to the grid for each component, and the key for each component is added to the grid for each component.

// components -> ButtonView -> GenerateForm.vue
<component ... :draggable="false"/>

// elements -> grid -> view.vue
<el-col>
    <draggable v-if='draggable' >.</draggable>
    <template v-else>
        <component .>
    </template>
</el-col>import components from "elements/view"; export default { beforeCreate(){ Object.assign(this.$options.components, components); }... }Copy the code

The viewForm. vue view is not displayed after the default value of the field attribute of the form element is configured, but almost all form elements pass value as a component. Widgets pass default value of the component to display. So raster form elements also display default values.

// elements -> input -> view.vue
<input :value='value' />

...
export default{...props: {
    value: {}}}// components -> ButtonView -> Widget.vue
<component
   ...
   :value='element.options.defaultValue'
   draggable
/>
Copy the code

Clicking Preview does not yet show the default values, and the default values must be bidirectionally bound to the form component. Therefore, you need to customize the V-Model of the form component element, declare the prop of the incoming component, and update the prop when the form value changes to trigger an event. Text and HTML are not bidirectional binding, but the internal component can still pass value, in addition to the splitter line does not need component pass value.

// elements -> input -> view.vue
<el-input ... 
    :value="value" 
    @input="value => $emit('change', value)" 
/>

export default{...model: {
    prop: "value".event: "change"
  },
  props: {...value: {},}}Copy the code

Generateform. vue inside is el-form, which introduces different form elements inside. The form elements are bidirectional binding, and then need to bind with data. Generateform.vue initializes models into the grid. Since objects pass value references, elements inside the grid can also be bound. Grids may be nested inside the grid, and models need to be passed.

// components -> ButtonView -> GenerateForm.vue
<el-form :model='models'. ><components 
        v-model='models[element.model]' 
        :models='models'
        . 
    />
</el-form>

export default {
  data(){
      return {
          models: {}}}.created() {
    this.handleSetModels();
  },
  handleSetModels() {
      var models = {};
      getGridModel(this.data.list);
      this.models = models;
      function getGridModel(list) {
        list.forEach((element) = > {
          if (element.type === "grid") {
            element.columns.forEach(column= > {
              if(column.list.length) { getGridModel(column.list); }}); }else {
            if(element.type ! = ="divider") { models[element.model] = element.options.defaultValue; }}}}// elements -> grid -> view.vue
<components 
   v-model='models[element.model]' 
   :models='models'. />export default {
    props: {models: {... }}}Copy the code

After clicking to get the data, put the Models data into the editor, and add the getData method inside generateForm. vue to return models. Models must be copied and returned to ensure that models in the component are not polluted.

// components -> ButtonView -> GenerateForm.vue
getData() {
      return deepClone(this.models)
}

// layout -> components -> ButtonView.vue
<generate-form :data="data" ref="generateForm". />handleGetData() {
      this.models = this.$refs.generateForm.getData();
      this.showPreviewData = true;
}
Copy the code

Tinymce rich text editor

The initial decision to use Tinymce as a rich text editor is mainly due to the fact that tinymce operation buttons are easy to control, image uploading is convenient, and Chinese documents are easy to use. The deficiency is that tinymCE relies on TinymCE-Vue and many function declarations need to be introduced separately to use, and component language needs to introduce JS separately. Image uploading from other editors is very complicated, and the most important thing is that the size of the uploaded image is uncontrollable, and some documents are not perfect. WangEditor may be replaced by other editors later, but it can be used as a trial.

Component configuration details can be refer to the source code, which picture upload section detailed description. Use images_upload_handler to customize image uploading. The parameters are blobInfo, success, failure. BlobInfo indicates the detailed information of the image file (filename, base64, etc.). Failure indicates a callback to an image uploading failure. User introduced generateForm. vue not visible custom image upload function body, if you want to obtain file information, callback function can only pass the value of the parent component through the child component, and after the introduction of raster must be passed up layer by layer. The editorUploadImage function retrieves file information and asynchronously calls the failure and success callback functions.

// elements -> editor -> view.vue
images_upload_handler: (blobInfo, success, failure) = > {
  this.$emit("editor-upload-image", {
     blobInfo,
     success,
     failure,
     model: this.element.model,
   });
}

// elements -> grid -> view.vue
<component
    @editor-upload-image=" data => $emit('editor-upload-image', data)"
/>

// components -> ButtonView -> GenerateForm.vue
<component
    @editor-upload-image=" data => $emit('editor-upload-image', data)"
/>

// layout -> components -> ButtonView.vue
<generate-form
   .
   @editor-upload-image="editorUploadImage"
/>

editorUploadImage({ model, blobInfo, success, failure }) {
    success("data:image/jpeg; base64," + blobInfo.base64());
}
Copy the code

8 blank User-defined area

If the design tool only supports the above form components, the design tool has great limitations. It does not support Tabs, tables, and third-party form components. Therefore, you need to provide custom area slots for users to insert different form components according to actual conditions to increase the ducsibility of the form.

If a component has multiple internal named slots, external elements will be inserted into the internal named slots. When multiple elements with different slot names are inserted outside a component, only the elements with the same slot name as those in the component can be inserted. When generateForm.vue is initialized, you need to not only create the bound form Models, but also get the Models of all the custom regions within the form, which is the name of the slots within the custom region.

If generateForm. vue inserts A, B, C and other elements with different slot names, the form contains custom region A and raster nested layers B. Generateform. vue creates A slots array (A and B) based on the number of custom areas in the form. Create named slots for A and B.

<generate-form>
    <div slot='A'>A</div>
    <div slot='B'>B</div>
    <div slot='C'>C</div>. </generate-form>// generate-form
<div>
    <blank-A>
        <template slot='A'>
            <slot name='A' />
        </template >
        <template slot='B'>
            <slot name='B' />
        </template >
    </blank-A>
</div>

// black-A
<div>
    <slot name='A'>
</div>
Copy the code

The slots array (A and B) is passed into the first grid. The first grid is internally parsed into A second grid and slots are created. The second grid is internally parsed into A custom region B and slots are created. The element B is inserted into the user-defined area B because only slot B is inside the user-defined area B.

<generate-form>
    <div slot='A'>A</div>
    <div slot='B'>B</div>
    <div slot='C'>C</div>. </generate-form>// generate-form
<div>
    <grid-1>
        <template slot='A'>
            <slot name='A' />
        </template >
        <template slot='B'>
            <slot name='B' />
        </template >
    </grid-1>
</div>

// grid1
<div>
    <grid-2>
        <template slot='A'>
            <slot name='A' />
        </template >
        <template slot='B'>
            <slot name='B' />
        </template >
    </grid-2>
</div>

// grid-2
<div>
    <black-B>
        <template slot='A'>
            <slot name='A' />
        </template >
        <template slot='B'>
            <slot name='B' />
        </template >
    </black-B>
</div>

// black-B
<div>
    <slot name='B'>
</div>
Copy the code

The above principle is basically close to source code, and the page is created to initialize Models and slots. The custom area places models on the Model field, no matter how many layers the grid is nested, scope.model is always Models, and Models is the same as models inside the form. The custom component binds models variables bidirectionally. Preview fetch data is always the form value Models (including custom components).

// components -> ButtonView -> GenerateForm.vue
<component :slots='slots'>
    <template 
        v-for="slot in slots" 
        :slot="slot" 
        slot-scope="scope"
    >
       <slot :name="slot" :model="scope.model" />
    </template>
</component>

handleSetModels() {
  var models = {};
  var slots = [];
  getGridModel(this.data.list);
  this.models = models
  this.slots = slots;
  function getGridModel(list) {
     list.forEach((element) = > {
      if (element.type === "grid") {... }else {
        if (element.type === "blank") { slots.push(element.model); }... }}}})// elements -> grid -> view.vue
<component :slots='slots'>
    <template 
        v-for="slot in slots" 
        :slot="slot" 
        slot-scope="scope"
    >
        <slot :name="slot" :model="scope.model" />
    </template>
</component>

// elements -> blank -> view.vue
<div>
    <slot :name="element.model" :model="models">
       <div class="custom-area">{{ element.model }}</div>
    </slot>
</div>
Copy the code

9 Form Functions

9.1 reset

The form reset is basically done by calling the El-Form resetFields method, but the time and date picker resets are buggy. In time range, bind value times, initial value null, reset after selecting time, times value is [null], bind value null array [], open time picker cannot select time. This bug exists in all date ranges, so the default value of range selection method is null, and the form is rewritten internally.

<el-form :model='form' ref="form">
  <el-form-item prop='times' label="Time frame">
    <el-time-picker
      value-format="HH-mm-ss"
      is-range
      v-model="form.times"
    ></el-time-picker>
  </el-form-item>
</el-form>

<el-button @click="$refs.form.resetFields()">reset</el-button>

export default {
    data(){
        return {
            form: {times: null}}}}// components -> ButtonView -> GenerateForm.vue
reset() {
  this.$refs.modelsForm.resetFields();
  this.resetTimePicker();
}

resetTimePicker() {
  for (const key of Object.keys(this.models)) {
    if (
      Array.isArray(this.models[key]) &&
      this.models[key].length === 1 &&
      this.models[key][0= = =null
    ) {
      this.models[key] = null; }}}Copy the code

9.2 Verification Rules

Validation rules are mostly mandatory, with single-line text and multi-line text being the only exceptions. Single-line text supports validator validation and regular expression validation, and multi-line text supports regular expression validation. The required field asterisk is controlled by the component pass value EL-fom-item required attribute.

<el-form-item :required="element.options.required">... </el-form-item>Copy the code

There are four types of elder-UI el-form validation methods, the most common of which is mandatory. If required is set to true, regular expression pattern can be specified directly. Validators include string, number, interger, float, URL, hex, and email. You can customize the authentication rules. For details, see Element-UI.

rules: {
  name: [{required: true.message: 'Please enter your name'}].phone: [{pattern: /^1[3456789]d{9}$/, message: 'Out of format'}].email: [{type: 'email'.message: 'Out of format'}].psw: [{validator: validatePsw }
 ]
}
Copy the code

When the form is initialized, not only models and slots are created, but also the validation rules are created. Required mode is added to all properties, but required is not turned on if it is false. IsPattern is the regular validation mode, which is unique to single-line text and multi-line text. New RegExp instantiates the regular.

// components -> ButtonView -> GenerateForm.vue
<el-form>

handleSetModels(){...var rules = {};
      getGridModel(this.data.list);
      this.rules = rules;
      function getGridModel(list) {
        list.forEach((element) = > {
          if (element.type === "grid") {... }else {
             rules[element.model] = [
              {
                required:!!!!! element.options.required,message:
                  element.options.requiredMessage || ` please enter${element.name}`,},];if (element.options.isType) {
              rules[element.model].push({
                type: element.options.type,
                message:
                  element.options.typeMessage || `${element.name}Validation does not match '}); }if (element.options.isPattern) {
              rules[element.model].push({
                pattern: new RegExp(element.options.pattern),
                message:
                  element.options.patternMessage || `${element.name}Format does not match ',})}}})}Copy the code

Number, integer, or floating point are special validators. If you specify one of them, form validation will not pass because the single-line text bidirectional binding value is always a string, not a number. Solution Specify That the el-input type must be specified as a number and the change event must be converted to a value.

<common-view ... ><el-input 
        type='number'
        @input='input'
        v-if='["number", "integer", "float"].includes(element.options.type)'
    />
    <el-input @input='input' v-else/>
</common-view>

input(value) {
  const type = this.element.options.type;
  if (["number"."integer"."float"].includes(type) && value ! = ="") {
     value = Number(value);
  }
  this.$emit("change", value);
}
Copy the code

9.3 Generating Code

The code generation part needs to populate the fixed template with data based on the form JSON, including submit and reset buttons by default, jsonData as the form’s JSON data, editData as the form’s initial value, and remoteOption as the cascading selector list data.

// elements -> cascader -> view.vue
<el-cascader
    ...
    :options="remoteOption && remoteOption[element.options.remoteOption]"
>
Copy the code

If the component uses a rich text editor, the default contains the event editor-upload-image and its corresponding handler.

editorUploadImage({ model, blobInfo, success, failure }) {
    // success(' picture SRC ')/failure(' failure description ') can be called asynchronously
    // success('http://xxx.xxx.xxx/xxx/image-url.png')
    // failure(' upload failed ')
    success('data:image/jpeg; base64,' + blobInfo.base64());
}
Copy the code

The slots section is inserted into the component based on the slots list and contains the slot name and variable binding by default. EditData is passed into the component. Generateform. vue incorporates default values and editData.

this.models = Object.assign(models, deepClone(this.value))
Copy the code

9.4 HTML Default value

The HTML default, which was originally placed in a multi-line text box, needs to be adjusted after the AceEditor is introduced. The default HTML values need to be updated after internal changes in the AceEditor, triggered by the change event.

// compoents -> AceEditor.vue
this.editor.session.on("change".(delta) = > {
   this.$emit("change".this.getValue());
})

// elements -> html -> config.vue
<ace-editor @change="handelChange". />handelChange(text) {
   this.data.options.defaultValue = text;
}
Copy the code

If the form contains multiple HTML components, the switching component finds that the internal values of the AceEditor have not changed. The HTML field attributes are always the same AceEditor, so the value does not change. Internally, you need to listen for changes to the HTML object by calling the component’s internal setValue method. The default object is not depth listening, but the depth listening cannot be enabled. The reason is that when the change event inside the component triggers updating the default value, the Hander will trigger calling setValue inside the component again after the change event is enabled, and the setValue will trigger change again. Causes the loop call page to freeze. That is, listen for object reference change, do not listen for object content change.

data: {
   handler() {
     this.$nextTick(() = > {
       this.$refs.htmlAceEditor.setValue(this.data.options.defaultValue);
     });
   },
   deep: false.immediate: true
}
Copy the code

10 Release and Maintenance

10.1 NPM components

Publish the project as an NPM package for later use. Create index.js in the same directory as mian. Js to import the components and styles that you want to export. Use (CPN) By default, the install method on CPN is invoked to register components. The default parameter is Vue.

// index.js
const components = [
    GenerateForm,
    MakingForm
]

const install = (Vue) = > {
    components.forEach(component= > {
        Vue.component(component.name, component)
    })
}

export default {
    install
}

// Reference mode
import DwFormMaking from 'dw-form-making'
Vue.use(DwFormMaking)
Copy the code

How component parts are imported.

// index.js
import GenerateForm from 'components/ButtonView/GenerateForm'
import MakingForm from './layout/index'

export {
    GenerateForm,
    MakingForm
}

// Call mode
import { GenerateForm, MakingForm } from 'dw-form-making'

Vue.component(GenerateForm.name, GenerateForm)
Vue.component(MakingForm.name, MakingForm)
Copy the code

New script command, name is the build name, the last parameter is the entry file. Refer to the vuecli library for details of parameters and build library output files.

// package.json
"scripts": {..."publish": "vue-cli-service build --target lib --name DwFormMaking ./src/index.js"
}
Copy the code

Configure package.json to describe in detail the fields required to publish the package.

  • name: package name, name is unique and available innpmCheck whether the official website is repeated
  • description: description,npmThe package description is displayed on the official website
  • version: Version number. The version number of each release cannot be the same as the historical version number
  • authorAuthor:
  • private: Whether private,falseTo publish to the publicnpm
  • keywords: Keyword, usually usednpmKeyword search
  • main: entry file, pointing to the compiled package file
  • files:npmWhitelist, onlyfilesThe files or folders specified in the

After applying for an official NPM account, NPM login logs in to NPM and runs NPM publish in the root directory.

10.2 Git Multi-remote Library Maintenance

Github and Gitee create repositories (initialized with Readme file), delete unnecessary remote libraries if associated. Then associated with Gitee and Github respectively, gitee service Gitee Pages can preview web Pages, deploy lib directory update.

// Check the remote library information
git remote -v

// Delete the remote library
git remote rm origin

// Associate the GiHub remote library
git remote add github https://github.com/username/repo.git

/ / push a lot
git push github master

// Pull the remote branch
git pull github master
Copy the code

11 afterword.

The basic version of the open source form designer is very small in scope, has a lot of bugs inside the designer, and even the most basic grid is not supported. One day when I was idle, I suddenly became interested in its source code. After a rough review, I found that its business logic was complicated, the level of components was very deep, and some of the components had redundant codes. Even the internal codes of a single component were close to 500 lines, with poor readability and expansibility. So reference its style, the DIRECT reconstruction of JS part.

The most basic form components are basically implemented and can be previewed, and the more complex grid layout needs to be carefully combed. With an understanding of the recursive nesting logic of the grid, it can be implemented soon. The custom region based on this, that is, the scope slot in the recursive component, is the most time-consuming, because it is a tool to do in free time, a little thought of working time will implement a demo, considered the render function, also considered to narrow the component hierarchy, the final slot V-for is also accidentally thought of at some point. The project previously included a selection tree, a code editor, and was carefully considered before deciding to delete it. The biggest reason is to reduce the size of the project, in which more components are just perfect, and the differences are just similar. Different basic components are involved, and customized components are introduced, which is simple but not simple.

The overall difficulty of the project is not high, and this note is only part of the idea of documenting the refactoring process. The first refactoring was interesting in part because of its internal logic and the novelty of the NPM package release. As a whole, it reinforces the use of the Element-UI form component, as well as some other components, and is an exercise in page layout, class name Settings, and code specification. Recursive component, scope slot, component circular reference is more complex, careful combing can also understand the principle. Code management can also consolidate the use of Git basic commands, multi-remote library management.

The tool can be previewed or cloned online. The repository has been rebuilt because of the large version library caused by too many Git submissions. Source code in Gitee and GitHub open source, tool name DW-form-making.

12 Updating Logs

12.1 20/12/11 was

As you may have noticed, the store in the code is all imported and reused, why not put it in the vue prototype object, the code will restore the way vuex calls to the maximum extent.

import store from './store'
Vue.prototype.$store = store
Copy the code

The first argument to the component install method is the vue constructor, which is the vue when vue.use () is executed. If you add $store to the vue prototype as described above, there is a bad case. The $store keyword is not used, the page must define other keywords, otherwise $store will be overwritten. Even worse are projects that reference vuex state management. Since Vuex injects $store in the first line of beforeCreate, integrating the form tool with it can cause the tool to crash and cause unexpected bugs.

This update fixes the raster copy key bug and removes some unreferenced variables from the component. And printed eggs in the tools console (try it), not including the NPM component.