preface

Schema => form. Schema is a JSON object. Let’s take a look at ali’s Form-Render library (a react form library).

{
  "type": "object"."properties": {
    "count": {
      // Base attribute
      "title": "Code"."type": "string"."min": 6.// rules (add verification information)
      "rules": [{"pattern": "^[A-Za-z0-9]+$"."message": "Only letters and numbers are allowed"}].// props (props for antd)
      "props": {
        "allowClear": true}}}}Copy the code

Although the official website says that this JSON follows the international JSON Schema specification, I think this specification is too troublesome. I define the Schema according to the usage habit of Ant-Design, which is more in line with the usage habit. For example, Ant uses components in this way. Vue’s elementUI seems to be used in a similar way:

<Form here you can define the properties of the Form ><Form.Item name="account"You can define it hereForm.ItemThe properties of >
        <InputHere you can define the properties of the form component
    </Form.Item>
    <Form.Item name="password">
         <InputHere you can define the properties of the form component
    </Form.Item>
</Form>
Copy the code

So the corresponding schema definition is similar to that used by the component:

{
    // Equivalent to the attributes defined on the Form component above
    formOptions: {// Retain the field value when the field is deleted
        // preserve:true
    }, 
    formItems: [ // This is equivalent to all form.item components in the Form component
      {
        // This attribute is so important that it is required. It is equivalent to the identifier of each component, which can be an array
        // An array can be a string or a number, which defines nested objects, nested arrays
        name: 'account'.// value: ", where an initial value can be defined or not set
        options: { // Equivalent to the form.item component property
           // hidden: xx hides the form logic
        }, 
        // The layout properties, which are then used to control the layout of the component
        // The layout attribute is the UI attribute that sets a row or column of the form, the width and height of the form label, etc
        // You can see that we decoupled the UI properties from the logical form properties
        // This attribute is not covered in this article
        layoutOptions: { // Leave the properties of the layout component to be expanded later
            // label: xx
        }, 
        // Component name, where 'input' will be converted to the Ant Input component
        // There will be a map to convert strings to components
        Wiget: 'input'.WigetOptions: {}, // Form component properties},],}Copy the code
  • {a: {b: ‘change here’}} {b: ‘change here’}}

  • {a: [undefined, ‘change here’]},

By setting the name, we can see that it satisfies both array nesting and object nesting, so it satisfies almost all formatting requirements for form object values.

So we expect the form kernel to use something like this:

/ / define schema
const schema = {
  formItems: [{name: 'account'.value: 1.options: {},Wiget: 'input'}}]const Demo = () = > {
  const [form] = useForm({ schema });
  return <Form form={form} />;
};

ReactDOM.render(
  <Demo />.document.getElementById('app'));Copy the code

The above configuration renders an Input component, and the form provides a set of methods just like Ant does. GetFiledsValue, setFieldsValue, and so on make our use of Ant almost seamless.

Some people say, well, you can just use Ant, but you know,

However, some ant properties are functions that cannot be attached to JSON because json. stringify filters out functions. Therefore, many Ant properties that need to be attached to functions are not supported internally, such as the onFinish event, shouldUpdate method, etc

In addition, if our business needs a lot of custom requirements for a product, may involve changing the underlying form library, you need to develop a set of their own, so it is not good to change ant form, it is better to develop a set of their own

Without further ado, start coding!

General architecture

Our general architecture is as follows (without writing the Form renderer (i.e. visualizing the drag and drop form)) :

Let’s set up the FormStore first. After all, it is the main scheduling component. To save time, we will not use TS, but js first.

The following are some of the utility functions and identifiers provided by the Ramda library. This is not important. You can guess what these functions mean by looking at the names of the functions

import { path, clone, assocPath, merge, type, equals } from 'ramda'

// Here are some identifiers
// This identifier is meant to notify all components of updates
const ALL = Symbol(The '*');
// This identifier is used to identify formStore
const FORM_SIGN = Symbol('formStoreSign');
// Export the identifier of the internal method
const INNER_HOOKS_SIGN = Symbol("innerHooks");
Copy the code

FormStore

Used to store form data, accept form initial values, and encapsulate operations on form data.

class FormStore {
  // The parameters are initialized values
  constructor(initialValue) {
    // There is a resetValue, which is the method to reset the form, so keep it
    this.initialValue = initialValue
    
    // values Stores the values of the form
    // The clone function is provided by Ramda
    this.values = initialValue ? clone(initialValue) : {}
    
    // Event collector, where subscribed events (functions) are stored
    this.listeners = []
  }
}
Copy the code

The Form data warehouse, FormStore, communicates with each form. Item (which wraps forms such as Input components and registers Input in FormStore) in a publisit-subscribe mode. Each Field is created by calling formStore.subscribe (Listener) to subscribe to the form data changes

  // Notifies one or all components to update form values
  notify = (name) = > {
    for (const listener of this.listeners) listener(name)
  }

  // A function that returns a function that clears the event subscribed to by the component when it is unloaded
  subscribe = (listener) = > {
    this.listeners.push(listener)
    return () = > {
      // Unsubscribe
      const index = this.listeners.indexOf(listener)
      if (index > -1) this.listeners.splice(index, 1)}}Copy the code

Some things to note above:

  • this.notify(name)In thenameWhich can be an array or a string, such as['account', 'CCB'].['account', 0]

Add the getFieldValues, getFieldValue, setFieldValue, and setFieldsValue functions.

  • GetFieldValues: Gets the value of the entire form item
  • GetFieldValue: Gets the value of a single form item
  • SetFieldValue: Sets the value of a single form item, where thenotify(name), notifying individual form updates
  • SetFieldsValue: Sets the value of multiple form items where thenotify(name)To ensure that all form changes trigger notifications
  // Get the form value
  getFieldValues = (name) = > {
    return clone(this.values)
  }

  / / the name here is not necessarily a string, it is possible that an array of strings, or an array subscript (string | string | number [])
  // For example: name = ['a', 'b'] specifies the value[a][b] attribute of the form value object
  getFieldValue = (name) = > {
    if (typeofname ! = ='string'&&!Array.isArray(name)) {
      throw new Error(` parameters${name}It needs to be a string or an array ')}// strToArray is defined below, which is the function that converts to an array
    // Since the first argument of path must be an array, name may be a string
    // path:
    // path(['a', 'b'], {a: {b: 2}}) => 2
    return path(strToArray(name), this.values)
  }

  // Method to set a single value of the form
  setFieldValue = (name, value) = > {
    const newName = strToArray(name)
    // assocPath is the function that Ramda uses to set the value of an object
    // assocPath:
    // assocPath(['a', 'b', 'c'], 42, {a: {b: {c: 0}}})
    // => {a: {b: {c: 42}}}
    this.values = assocPath(newName, value, this.values)
    // Publish events. Our events are identified by a name string
    this.notify(name)
  }

  // A method to set multiple values for the form
  setFieldsValue = (value) = > {
    // If value is not an object (such as {}), this function does not execute
    if(R.type(value) ! = ='Object') return
    // The pickPath method resolves an object into a path
    // pickPaths({a: 2, c: 3 })
    // => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
    const paths = pickPaths(value)
    paths.forEach((item) = > {
      this.values = assocPath(item.path, item.value, this.values)
    })
    this.notify(ALL)
  }
Copy the code

Then there are the utility functions and the exported functions. The functions and functions are written in comments, so that the FormStore component is roughly complete.

 // Expose the formStore's internal methods to the outside, preventing them from accessing the formStore directly
  getFormExport = (schema) = > {
    return {
      signType: FORM_SIGN,
      getFieldValue: this.getFieldValue,
      setFieldValue: this.setFieldValue,
      setFieldsValue: this.setFieldsValue,
      isSamePath: this.isSamePath,
      getInnerHooks: this.getInnerHooks(schema),
    }
  }
  // Check whether the two paths are equal
  // equals([1, 2, 3], [1, 2, 3]); //=> true
  isSamePath = (path1, path2) = > {
    if(type(path1) ! = ='Array'|| type(path2) ! = ='Array') {
      throw new Error(The parameters of the isSamePath function must be an array)}return equals(path1, path2) //=> true
  }
  
  // Get an internal method that is only used in internal components
  getInnerHooks = schema= > sign= > {
    if(sign === INNER_HOOKS_SIGN) {
      return {
        getFieldValue: this.getFieldValue,
        setFieldValue: this.setFieldValue,
        setFieldsValue: this.setFieldsValue,
        isSamePath: this.isSamePath,
        subscribe: this.subscribe,
        notify: this.notify,
        schema
      }
    }
    console.warn('Externally disallow use of getInnerHooks');
    return null;
  }
// Here are the utility functions

// This function is to convert a string into an array
const strToArray = (name) = > {
  if (typeof name === 'string') return [name]
  if (Array.isArray(name)) return name
  throw new Error(`${name}The argument must be an array or string ')}// This function is used to extract the path of the object.
// pickPaths({a: 2, c: 3 })
// => [[{path: 'a', value: 2 }], [{ path: 'c', vlaue: 3 }]]
// pickPaths({ b:[ { a : 1 } ] )
// => [[ { path: [ "b", 0, "a"], value: 1 }]]
function pickPaths(root, collects = [], resultPaths = []) {
  function dfs(root, collects) {
    if (type(root) === 'Object') {
      return Object.keys(root).map((item) = > {
        const newCollect = clone(collects)
        newCollect.push(item)
        return dfs(root[item], newCollect)
      })
    }
    if (type(root) === 'Array') {
      return root.map((item, index) = > {
        const newCollect = clone(collects)
        newCollect.push(index)
        return dfs(item, newCollect)
      })
    }
    return resultPaths.push({ path: collects, value: root })
  }
  dfs(root, collects)
  return resultPaths
}
Copy the code

Ok, let’s see what we can do with the FormStore component we just wrote

const formStore = new FormStore({ account: [{name: 'CCB'}}]); formStore.setFieldsValue({account: [{name: 'xiaoming' }, 123]});/ / print formStore. Value
// => { account: [ { name: 123 }, 123 ] }
console.log(formStore.values)


formStore.setFieldValue([ 'account'.1.'age'].10)
// => { account: [ { name: 123 }, age: 10 ] }
console.log(formStore.values)
Copy the code
  • As you can see above, the path resolution module is very important to us, so I will separate it out as a service in the future. We also need to separate these important modules into service classes, or hooks, in our business code.

  • And then I’m going to write it as a function and then I’m going to refactor the function. This is just for those who don’t know functions and don’t know how to use the Ramda library.

Let’s try the formStore registration function again

const formStore = new FormStore({ account: [{ name: "CCB"}}]); formStore.subscribe((name) = >{ 
   if(name === ALL || formStore.isSamePath(name, [ 'account'.0.'name' ])){
   console.log('Path match [account, 0, name]')}})// formStore.setFieldsValue({ account: [{ name: "A" }] })
 // => Print path match [account, 0, name]
 formStore.setFieldValue([ 'account'.0.'name'].'A')
Copy the code

Ok, so this module is supposed to be my test case and I need to use the test library, so I’m not going to use it here, so you’re welcome to check out my upcoming JEST Primer in a couple of days. (Mainly to promote this, keep learning, great 😄)

The subscribe and notify publish events above are a simple publish subscribe model. To put it simply, like the source code of Redux, the subscribed event is to put the subscribed function into an array, and the published event is to take the function out of the array and call it again.

Let’s take a look at what the Form component looks like. The Form component is fairly simple and serves only to provide an entry and a passing context.

The Form component

Props receives an instance of the FormStore (generated by useForm({schema})) and passes it to the child component (Field) via Context

import { INNER_HOOKS_SIGN } form './utils';
import { FormContext } from './context';

// Form component mapping
const WigetsMap = {
  input: Input
}

function Form(props) {
  if(props.form.signType ! == FORM_SIGN)throw new Error('Form type error');
  // In this case, the form is the object generated by the useForm
  // This object is actually exported by the formStore's exportForm method
  // signType indicates the object exported by our formStore.exportForm method
  if(form.signType ! == FORM_SIGN)throw new Error('Form type error');
  // External form
  const{ form, ... restProps } = props;// Get the internal functions exported to fromStore by the getInnerHooks method
  const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
  
  return (
    <form
      {. restProps}
      onSubmit={(event)= >{ event.preventDefault(); event.stopPropagation(); // the submit method provided by formInstance is called // innerForm.submit(); }} > {/* formInstance as global context pass */}<FormContext.Provider value={innerForm}>{/* useForm */} {innerform.schema? .formItem? .map((item, index) => {return ({/* formItem attribute passed below */})<FormItem key={index} name={item.name} {. item.options} >{/* WigetsMap[item.wiget]?<item.Wiget {. item.WigetOptions} / > : null}
            </FormItem>
          );
        })}
      </FormContext.Provider>
    </form>
  );
}

Copy the code

getInnerHooks

The main function of the Form component is to pass the innerForm to the form. Item component.

        // Get an internal method that is only used in internal components
        getInnerHooks = schema= > sign= > {
          if(sign === INNER_HOOKS_SIGN) {
            return {
              getFieldValue: this.getFieldValue,
              setFieldValue: this.setFieldValue,
              setFieldsValue: this.setFieldsValue,
              isSamePath: this.isSamePath,
              subscribe: this.subscribe,
              notify: this.notify,
              schema,
            }
          }
          console.warn('Externally disallow use of getInnerHooks');
          return null;
        }
Copy the code

As you can see, the exported object must be obtained by passing in the INNER_HOOKS_SIGN identifier. INNER_HOOKS_SIGN is internal to the component and is not available to outside developers using UseForms, so the trace object only serves within the component.

The purpose is to get and set properties, subscribe to, and publish events.

So FormContext is the context, so what does this file look like

FormContext

import React from 'react'

const warningFunc: any = () = > {
    console.warn(
    'Please make sure to call the getInternalHooks correctly'
    );
  };
  
  export const FormContext = React.createContext({
    getInnerHooks: () = > {
      return {
        getFieldValue: warningFunc,
        setFieldValue: warningFunc,
        setFieldsValue: warningFunc,
        isSamePath: warningFunc,
        subscribe: warningFunc,
        notify: warningFunc, }; }});Copy the code

The default arguments are the getInnerHooks methods we define in the FormStore, which make sure they have the same names as the exported properties of the two functions. This is where typescript becomes important.

You’re welcome to check out my blog for a typescript primer

Next, let’s take a look at how the external useForm is used

useForm

const useForm = (props) = > {
  // Check whether the schema conforms to the specification. If not, an error is reported
  checkSchema(props.schema);
  // Save the value of the schema
  const schemaRef = useRef(props.schema);
  // Save the reference object for the form
  const formRef = useRef();
  
  // Initialize the formStore for the first render
  if(! formRef.current) { formRef.current =new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  // If the schema changes, the formStore is regenerated
  if (JSON.stringify(props.schema) ! = =JSON.stringify(schemaRef.current)) {
    schemaRef.current = props.schema;
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  return [formRef.current];
};

// Utility functions
function checkSchema(schema) {
  ifElse(
    isArrayAndNotNilArray,
    forEach(checkFormItems),
    () = > { throw new Error('formItems property of schema need to an Array') }
  )(path(['formItems'], schema));
}

function checkFormItems(item) {
  if(! all(equals(true))([isObject(item), isNameType(item.name)])) {
    throw new Error('please check whether formItems field of schema meet the specifications'); }}Copy the code

The only thing worth mentioning above is the use of useRef, which can be used as a singleton, as follows:

const a = useRef();
if(! a.current)return 1;
return a.current
Copy the code

The first time I assign a value of 1, if it exists it’s always going to be 1, it’s not going to change

Next, let’s look at the code for the form.item component

Form.Item

import React, { cloneElement, useEffect, useContext, useState } from 'react'
import { FormContext } from './context';
import { ALL } form './utils';

function FormItem(props: any) {
  const { name, children } = props;

  // This is to get the store Context, which will be explained later
  const innerForm = useContext(FormContext);

  // If our schema initialization has a value, it will be passed here
  const [value, setValue] = useState(name && store ? innerForm.getFieldValue(name) : undefined);

  useEffect(() = > {
    if(! name || ! innerForm)return;
    // If n is ALL, everyone should be updated
    // Update the form separately
    // Let n be the same as name
    return innerForm.subscribe((n) = > {
      if (n === ALL || (Array.isArray(n) && innerForm.isSamePath(n, name))) { setValue(store.getFieldValue(name)); }}); }, [name, innerForm]);return cloneElement(children, {
    value,
    onChange: (e) = >{ innerForm.setFieldValue(name, e.target.value); }}); }Copy the code

Note that the cloneElement wraps the children around its value and onChange methods, such as:

<Form.Item name="account"Here we can define the property > of form.item<InputHere you can define the properties of the form component
</Form.Item>
Copy the code

The Input here will automatically receive the value and onChange properties and methods

  • And the onChange method calls the setFieldValue method of the innerForm
  • This method will then call the method registered in useEffect for formItem, achieving the goal of updating the component individually without a global refresh

This article is completely interested in their own low code form platform form implementation principle, I checked some information, wrote a demo can run through, but the principle is no problem, there may be bugs inside, welcome to comment area, weekend is still writing articles, see in the hard part, brother point a like, 😀

The following code is refactored using the Ramda library. I ran it by myself and found no problems. The follow-up plan of this paper is as follows:

  • Join the typescript
  • Added jEST test function functionality
  • Added a visual form generation interface

Complete code ramda version

import ReactDOM from 'react-dom';
import React, { useState, useContext, useEffect, useRef, cloneElement } from 'react';
import { path, clone, assocPath, type, equals, pipe, __, all, when, ifElse, F, forEach, reduce } from 'ramda';
import { Input } from 'antd';

// Constant module
const ALL = Symbol(The '*');
const FORM_SIGN = Symbol('formStoreSign');
const INNER_HOOKS_SIGN = Symbol('innerHooks');

// Utility function module
function isString(name) {
  return type(name) === 'String';
}

function isArray(name) {
  return type(name) === 'Array';
}

function isArrayAndNotNilArray(name) {
  if(type(name) ! = ='Array') return false;
  return name.length === 0 ? false : true;
}

function isUndefined(name) {
  return type(name) === 'Undefined';
}

function isObject(name) {
  return type(name) === 'Object';
}

function strToArray(name) {
  if (isString(name)) return [name];
  if (isArray(name)) return name;
  throw new Error(`${name} params need to an Array or String`);
}

function isStrOrArray(name) {
  return isString(name) || isArray(name);
}

const returnNameOrTrue = returnName= > name= > {
  return returnName ? name : true;
}

function isNameType(name, returnName = false) {
  return ifElse(
    isStrOrArray,
    returnNameOrTrue(returnName),
    F,
  )(name)
}

function checkSchema(schema) {
  ifElse(
    isArrayAndNotNilArray,
    forEach(checkFormItems),
    () = > { throw new Error('formItems property of schema need to an Array') }
  )(path(['formItems'], schema));
}

function checkFormItems(item) {
  if(! all(equals(true))([isObject(item), isNameType(item.name)])) {
    throw new Error('please check whether formItems field of schema meet the specifications'); }}function setFormReduce(acc, item) {
  if(! isUndefined(item.value)) { acc = assocPath(strToArray(item.name), item.value, acc) }return acc;
}

function setSchemaToValues(initialSchema) {
  return pipe(
    path(['formItems']),
    reduce(setFormReduce, {})
  )(initialSchema)
}

const warningFunc = () = > {
  console.warn(
    'Please make sure to call the getInternalHooks correctly'
  );
};

export const FormContext = React.createContext({
  getInnerHooks: () = > {
    return {
      getFieldsValue: warningFunc,
      getFieldValue: warningFunc,
      setFieldValue: warningFunc,
      setFieldsValue: warningFunc,
      isSamePath: warningFunc,
      subscribe: warningFunc,
      notify: warningFunc }; }});function pickPaths(root, collects = [], resultPaths = []) {
  function dfs(root, collects) {
    if (isObject(root)) {
      return dfsObj(root)
    }
    if (isArray(root)) {
      return dfsArr(root)
    }
    return resultPaths.push({ path: collects, value: root })
  }

  function dfsObj(root) {
    Object.keys(root).map((item) = > {
      const newCollect = clone(collects)
      newCollect.push(item)
      return dfs(root[item], newCollect)
    })
  }
  function dfsArr(root) {
    root.map((item, index) = > {
      const newCollect = clone(collects)
      newCollect.push(index)
      return dfs(item, newCollect)
    })
  }
  dfs(root, collects)
  return resultPaths
}

class FormStore {
  constructor(initialValue) {
    this.initialValue = initialValue
    this.values = initialValue ? clone(initialValue) : {}
    this.listeners = []
  }
  getFieldsValue = () = > {
    return clone(this.values)
  }

  getFieldValue = (name) = > {
    return ifElse(
      isNameType,
      pipe(strToArray, path(__, this.values)),
      F,
    )(name, true)
  }
  setFieldValue = (name, value) = > {
    pipe(
      strToArray,
      (newName) = > {
        this.values = assocPath(newName, value, this.values);
        this.notify(name);
      },
    )(name)
  }

  setFieldsValue = (value) = > {
    return when(
      isObject,
      pipe(pickPaths, forEach((item) = > {
        this.values = assocPath(item.path, item.value, this.values)
      }), () = > this.notify(ALL)),
    )(value)
  }

  notify = (name) = > {
    for (const listener of this.listeners) listener(name)
  }


  subscribe = (listener) = > {
    this.listeners.push(listener)
    return () = > {
      const index = this.listeners.indexOf(listener)
      if (index > -1) this.listeners.splice(index, 1)
    }
  }


  getFormExport = (schema) = > {
    return {
      signType: FORM_SIGN,
      getFieldValue: this.getFieldValue,
      setFieldValue: this.setFieldValue,
      setFieldsValue: this.setFieldsValue,
      isSamePath: this.isSamePath,
      getFieldsValue: this.getFieldsValue,
      getInnerHooks: this.getInnerHooks(schema)
    }
  }


  isSamePath = (path1, path2) = > {
    if(type(path1) ! = ='Array'|| type(path2) ! = ='Array') {
      throw new Error('All arguments to the isSamePath function need an array')}return equals(path1, path2)
  }


  getInnerHooks = schema= > sign= > {
    if (sign === INNER_HOOKS_SIGN) {
      return {
        getFieldsValue: this.getFieldsValue,
        getFieldValue: this.getFieldValue,
        setFieldValue: this.setFieldValue,
        setFieldsValue: this.setFieldsValue,
        isSamePath: this.isSamePath,
        subscribe: this.subscribe,
        notify: this.notify,
        schema
      }
    }
    console.warn('Externally disallow use of getInnerHooks');
    return null; }}const useForm = (props) = > {
  checkSchema(props.schema);
  const schemaRef = useRef(props.schema);
  const formRef = useRef();
  if(! formRef.current) { formRef.current =new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  if (JSON.stringify(props.schema) ! = =JSON.stringify(schemaRef.current)) {
    schemaRef.current = props.schema;
    formRef.current = new FormStore(setSchemaToValues(props.schema)).getFormExport(props.schema);
  }
  return [formRef.current];
};

function FormItem(props) {
  const { name, children } = props;

  // This is to get the store Context, which will be explained later
  const innerForm = useContext(FormContext);

  // If our new FormStore has
  const [value, setValue] = useState(name && innerForm ? innerForm.getFieldValue(name) : undefined);

  useEffect(() = > {
    if(! name || ! innerForm)return;
    return innerForm.subscribe((n) = > {
      if (n === ALL || (Array.isArray(n) && innerForm.isSamePath(n, strToArray(name)))) { setValue(innerForm.getFieldValue(name)); }}); }, [name, innerForm, innerForm]);return cloneElement(children, {
    value,
    onChange: (e) = >{ innerForm.setFieldValue(name, e.target.value); }}); }const WigetsMap = {
  input: Input
}

function Form(props) {
  if(props.form.signType ! == FORM_SIGN)throw new Error('Form type error');

  const{ form, ... restProps } = props;const innerForm = form.getInnerHooks(INNER_HOOKS_SIGN);
  return (
    <form
      {. restProps}
      onSubmit={(event)= > {
        event.preventDefault();
        event.stopPropagation();


      }}
    >
      <FormContext.Provider value={innerForm}>
        {innerForm.schema.formItems.map((item, index) => {
          return (
            <FormItem key={index}
              name={item.name}
              {. item.options}
            >
              {WigetsMap[item.Wiget] ? <item.Wiget {. item.WigetOptions} / > : null}
            </FormItem>
          );
        })}
      </FormContext.Provider>
    </form >
  );
}

const schema = {
  formItems: [
    {
      name: 'account',
      value: 1,
      options: {
      },
      Wiget: 'input'
    }
  ]
}

const Demo = () => {
  const [form] = useForm({ schema });
  window.f = form;
  return <Form form={form} />;
};

ReactDOM.render(
  <Demo />,
  document.getElementById('app')
);

Copy the code