motivation

Development dynamic form before related applications and visual designer, but found that if from a development perspective, no matter how rich functions, provide to the interface implementation more flexible and diversified designer would need to be further extended to add more features, so that the designer function becomes more and more bloated, learning to increase the cost of using the designer, More complex interaction requirements are often not as easy to implement as direct code development

Visual editing is generally only applicable to people who are not specialized in development work, and the requirements of interface interaction logic are highly reusable, which cannot cover all requirements for complex application scenarios, especially highly customized situations

The data defined in some configuration files is in YAML format, which seems to be easier to edit than json data, so WE decided to explore whether yamL data can be used to directly build the interface to reduce development costs

And then there’s the 1024 days of the year. Instead of going to all sorts of weird games and dinners, how can you be more pragmatic about the issues at hand

Results the preview

Based on the concept of “VJDesign – Vue interface Visual Designer”, we first developed a yamL editor demo, which can theoretically support any HTML element and any component library developed based on VUE3, currently integrated with the Element-Plus component library

Jrender-plus.net lify.app/ is an exploratory stage, and some features will be updated later

Since the project uses tailwindcss, some native HTML components such as input h1 will have no border and no default style when there is no style defined. As long as the style is added separately, it will not affect the function

Rule description

The definition rules are sorted out based on the sample demo

Basic attributes

The root level of yamL data definition includes datasource Fields

A datasource is a datasource. The data on the current interface can be defined here

Listeners are used to monitor and respond to changes in data

Fields is the definition of an interface layout

attribute type describe
datasource object Provides data and HTTP access
listeners array Listen for property changes and perform response actions
fields array Interface Layout Definition

Using the component

Each node is a component, and the basic properties of the component are defined

attribute type describe
component string Component name, which can be the name of any HTML element or VUE component
props object Component properties, which can be attributes of HTML elements or VUE components, are defined in HTML or VUE component library documents
children array Collection of subcomponents

Example 1: Show a layer with a border

fields:
  - component: div
    props:
      style:
        width: 200px
        border: 1px solid red
    children:
      - component: p
        props:
          innerText: content
Copy the code

Effect:

Example 2: Vue Slot feature

Supports setting slot properties in components to render into the corresponding slots of VUE components

fields:
  - component: el-input
    model: model.text
    props:
      style:
        width: auto
    children:
      - component: span
        slot: append
        props:
          innerText: append
Copy the code

Effect:

The data source

Datasource refers to the data available in the current interface and is defined in datasource. Resources include objects, object attributes and remotely acquired data. Currently, object supports only defined data, while fetch can initiate HTTP requests to obtain remote data or send data to services

Object data source

An object is defined as follows: In the datasource, an object is defined. The properties of each object are the properties that the object can use on the interface. The value of each object can be an object, an array, or a value

datasource:
  rawdata:
    props:
      prop1: aaa
      prop2: 123

  listdata:
    props:
      - name: sel1
        value: 1
      - name: sel2
        value: 2
Copy the code

Data source definitions can be used directly in component property expressions as follows

fields:
  - component: p
    props:
      innerText: $:rawdata.prop1
  - component: p
    props:
      innerText: $:rawdata.prop2
Copy the code

Model is the default data source, which is an object that can be assigned as model.< property > in a property expression

Fetch the data source

Fetch data source is used to provide HTTP request capability, so that data can be fetched from the server or sent to the service. Data source Type is defined as FETCH, which represents a FETCH data source

The PROPS property of the FETCH data source is defined as follows

attribute type instructions
url string Requesting a resource URL
autoLoad boolean Whether to automatically request after the interface is loaded
type string The data typejson text
props object Options for HTML native fetch
defaultData array/object Default data after no request or request failure
data array/object The result data of a request is usually only used in an expression

The FETCH data source also has methods

methods instructions
clear Empty data
fetch The initiating

Example 1: Request and display list data

datasource:
  tabledata:
    type: fetch
    props:
      url: /data/table.json
      autoLoad: true
      type: json
      props:
        method: GET

fields:
  - component: div
    children:
      - component: el-button
        props:
          innerText: reload
          onClick: | $:() => { tabledata.clear() tabledata.fetch() }  - component: el-table
    props:
      data: $:tabledata.data
    children:
      - component: el-table-column
        props:
          label: name
          prop: name
      - component: el-table-column
        props:
          label: remark
          prop: remark
Copy the code

Effect:

Listening to the

Listeners are defined in listeners to respond to changes in data and trigger actions. Currently, they can listen to object properties in the data source (consider whether to add component ref to support direct listening to component properties)

Monitoring is generally defined as follows:

listeners:
  # listen on one
  - watch: $:model.num
    actions:
      - handler: $() = > console.log(model.num)
  # listen on multiple
  - watch:
      - $:model.text
    actions:
      - handler: $() = > console.log('xxx')
      - handler: $() = > tabledata.fetch()
        condition: $:model.text && model.text.length = = = 5
Copy the code

The attributes of a listener include the following

attribute type instructions
watch array/object The data to listen for
actions array Triggered behavior
deep boolean The depth of the listening
immediate boolean Execute immediately after the interface is loaded

The definition of an action action in Actions includes the following properties

attribute type instructions
handler function Behavior execution method
condition boolean The condition that triggers the operation
timeout number Method execution delays (which may be less common)

The condition and timeout attributes can be replaced by the handler, but they should be kept in place for the sake of writing less code

Function extension

Using expressions

Any property in a component, data source, or listener can use expressions for data linkage

@model.value:arguments[0] #: text ${content} #: text ${content} #: text ${content} In fact, one form is sufficient for the expression

Using $:< content > to represent an expression, attributes defined in this form are parsed into program implementations, either as property values or as arrow functions by writing $:() => {} directly

Example 1: Attribute value linkage

fields:
  - component: input
    props:
      style:
        border: 1px solid silver
      value: $:model.text
      onInput: $:(e)=>model.text=e.target.value
  - component: p
    props:
      innerText: $:model.text
Copy the code

Effect:

Example 2: Implement click events

Can pass before the attribute value added in yaml | to input line of text, more complex script can write a new line

fields:
  - component: button
    props:
      style:
        border: 1px solid
        padding: 0.75rem 1.25rem
      innerText: show
      onClick: | $:() => { alert('message') }Copy the code

Effect:

functions

Function functions, similar to the formula functions in Excel that provide some common function operations in expressions, now support defining an object containing methods in the data source that can be used in expressions, so common functions are less important

There is only one case to note here. In the js expression aa.bb.cc.dd = value, an error is reported if the parent property is empty or undefined. Therefore, the SET function is provided to SET the value in depth

fields:
  - component: input
    props:
      value: $:model.obj.text
      onInput: $:(e) = > SET(model, 'obj.text'. e.target.value)
  - component: p
    props:
      innerText: $:model.obj.text
Copy the code

Simplified null judgment logic

Quick properties

Extensions that integrate several commonly used properties further simplify component definitions

Example 1: VUE component V-Model

Component values are displayed and updated by associating the modelValue attribute and responding to the Update :modelValue event, using the Model attribute to simplify the definition

fields:
  - component: el-input
    model: model.text
    props:
      style:
        width: auto
  - component: p
    props:
      innerText: $:model.text
Copy the code

Example 2: Simplified element-Plus form item definition

Define the formItem attribute on the component to automatically add el-form-item to the outer layer

fields:
  - component: el-form
    props:
      labelWidth: 120px
    children:
      - component: el-input
        model: model.text
        formItem:
          label: input1
        props:
          style:
            width: auto

      - component: p
        formItem:
          label: display
        props:
          innerText: $:model.text
Copy the code

Effect:

Example 3: Control element show hide

Sets the condition attribute to control whether the element is displayed

fields:
  - component: el-switch
    model: model.checked
  - component: p
    condition: $:model.checked
    props:
      innerText: hello!!
Copy the code

Effect:

The for loop

Example 1: For loop display

When elements need to be looping over array data, you can use the for attribute

datasource:
  listData:
    props:
      - aaaaa
      - bbbbb
      - ccccc

fields:
  - component: ul
    children:
      - component: li
        The second parameter name is not currently supported
        for: item in listData
        props:
          innerText: $:`${item} - ${index}`
Copy the code

Other features

You just have to be more imaginative and experiment, and you can discover more characteristics

Example 1: Add a style tag and set the style

fields:
  - component: span
    props:
      class: custom
      innerText: Custom styles in the page

  - component: style
    props:
      innerText: | .custom { color: red }Copy the code

Example 2: Define methods in datasource with expressions and associate them with drop-down lists

datasource:
  list:
    props:
      - m1
      - m2
  methods:
    props:
      m1: $() = > { Alert (' 1 'output) }
      m2: $() = > { Alert (' output 2 ') }

fields:
  - component: el-select
    model: model.sel
    children:
      - component: el-option
        for: item in list
        props:
          value: $:item
          label: $:item
  - component: el-button
    props:
      onClick: $:methods[model.sel]
      innerText: Choose to perform
Copy the code

Effect:

Rely on

library version
vue ^ 3.2.20
monaco-editor ^ 0.29.1
monaco-yaml ^ 3.2.1
file-saver ^ at 2.0.5
js-yaml ^ 4.1.0
element-plus ^ 1.1.0 – beta. 24
ejs ^ 3.1.6

about

scalability

The sample was developed using an extensible design pattern similar to the previous VJDesign-VUE interface visual designer, referring to the exported interface code

// Global extensions that are enabled for any rendering component in the current environment
useGlobalRender(
  ({ onBeforeRender, onRender, addDataSource, addFunction, addComponent }) = > {
    // Expression parsing is not done before the current component is rendered
    onBeforeRender(() = > (field, next) = > {
      next(field)
    })

    // The expression has been parsed before the current component is rendered
    onRender(() = > (field, next) = > {
      next(field)
    })

    // Add a type of data source
    addDataSource("Type".(options) = > {})

    // Add a function function
    addFunction("Name".(callback) = > (. args) = > {})

    // Add support for custom components (not globally registered in vue only for current rendering, not globally registered)
    addComponent("Name", Component)
  }
)

// Root-level extensions, which use extensions registered with this method and are enabled for any rendering component in the child node
useRootRender(
  ({ onBeforeRender, onRender, addDataSource, addFunction, addComponent }) = > {
    // ...})Copy the code

Can you export vUE components directly

Also consider this problem before, but the vue components quite so already compiled good static files, each component were fixed compiled component types and attributes, because components in the solution for each component before rendering preprocessing, process the results may be change the component’s attribute values, or change to render the component directly and change the child elements, The final output could be an implementation similar to
Continuous improvement

This thing is still under study, but I am too busy to have much time now. If you have new ideas and suggestions, you can follow up again and see the response level

For example, by defining an interface with YAML that is reused as a component in another interface, or by defining a slot in a component where the children element is exported to slot (the slot feature of Vue), So far we’ve implemented it all but we haven’t figured out how to do it in the editor yet