How We Built a Self‑Service Form Builder for Non‑Developers Using Formily

Facing frequent interruptions from operations requesting custom forms, we created a self‑service form‑building platform that lets non‑technical staff configure forms visually, using a decoupled iframe editor, postMessage communication, automatic JSON schema generation from TypeScript definitions, and both SchemaReactions and Effects for dynamic logic.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How We Built a Self‑Service Form Builder for Non‑Developers Using Formily

Preface

Because our business often needs to issue form‑type tasks to stores, all forms are configured on the frontend using Formily, we frequently interact with operations staff. While writing code, we would receive urgent messages from operations asking for new forms, forcing us to hand‑write configurations and waste valuable time.

We decided to develop a form‑building platform that allows operations staff to configure forms themselves without developer involvement.

Architecture Design

At the start we debated whether to build our own platform or use the official Designable; we chose to develop our own for three reasons:

Designable is too heavy for our business; many features are unnecessary.

Our internal business requires many customizations, and adapting Designable would be costly.

We need a platform suited for operations, while Designable is more developer‑oriented.

Based on this decision we designed a technical solution with the following requirements:

Support visual drag‑and‑drop (WYSIWYG).

Allow use of all components from the internal component library.

Minimize learning curve for quick onboarding by operations.

Visual Builder

The visual builder works by establishing a communication channel between the main site and the editor. User actions (add component, sort, delete, etc.) are converted into data messages sent to the editor, which updates the form schema dynamically.

Communication Design

We decouple the main site and the editor using an iframe and postMessage. The editor renders the form directly with Formily and the component library, so the main site does not need to load those resources, ensuring style consistency.

// admin to editor
export function postMsgToChild(message) {
  document.getElementById('frame')?.contentWindow?.postMessage(message, '*');
}

// editor to admin
export function postMsgToParent(message) {
  window.parent.postMessage(message, '*');
}

postMsgToChild({
  action: 'add',
  data: { componentName: 'xxx', props: {} }
});

window.addEventListener('message', (e) => {
  const { action, data } = e.data;
  switch (action) {
    // add/remove/copy/sort/changeProps/changeReactions
    case 'add':
      // todo
      break;
  }
});

Editor Design

The editor holds the list of available components and their configurations. Core structure (React state) includes components array, current index, and functions for add, remove, copy, sort, changeProps, changeReactions, and converting components to JSON schema.

const [components, setComponents] = useState([]) // user‑added components
const [curIndex, setCurIndex] = useState(0) // currently selected component index

// add
const add = (data) => {}
// remove
const remove = (data) => {}
// copy
const copy = (data) => {}
// sort
const sort = (data) => {}
// modify props
const changeProps = () => {}
// modify reactions
const changeReactions = () => {}
// convert components to jsonSchema for Formily rendering
const components2JsonSchema = (data) => {}

Component Configuration

Each component’s configuration describes all supported props. Example for an Input component:

const componentSchema = {
  "name": "Input",
  "description": "输入框",
  // component configurable props description
  "props": {
    "type": "object",
    "properties": {
      "placeholder": {
        "title": "placeholder",
        "x-decorator": "FormItem",
        "x-decorator-props": { "tooltip": "占位符" },
        "x-component": "Input",
        "x-component-props": {}
      }
      // other properties ...
    }
  }
};

Form Properties

Form properties configure Formily form attributes such as title, required, enum, etc.

const formSchema = {
  "title": "表单属性",
  "type": "object",
  "properties": {
    "title": {
      "title": "标题",
      "type": "string",
      "x-component": "Input",
      "x-decorator": "FormItem"
    }
    // other properties ...
  }
};

Layout Properties (Decorator Props)

Layout properties configure FormItem props, e.g., showing a colon.

const decoratorSchema = {
  "props": {
    "type": "object",
    "properties": {
      "colon": {
        "title": "colon",
        "x-decorator": "FormItem",
        "x-decorator-props": { "tooltip": "是否显示label右侧冒号" },
        "x-component": "Switch"
      }
      // other properties ...
    }
  }
};

Finally we send a complete component configuration JSON to the main site.

const postMsg = {
  type: 'props',
  data: {
    // right‑side property panel form rendering
    schema: {
      type: 'void',
      'x-decorator': 'FormItem',
      'x-component': 'FormCollapse',
      'x-component-props': { formCollapse: '{{formCollapse}}' },
      properties: { formSchema, componentSchema, decoratorSchema }
    },
    props: { formSchema: {}, componentSchema: {}, decoratorSchema: {} }
  }
};

Automatic Component Schema Generation

Manually writing a schema for each component is unsustainable. We use typescript-json-schema and fast-typescript-to-jsonschema to convert TypeScript definitions into JSON schema, extracting @title, @description, and @default annotations.

export interface Test {
  /**
   * @title this is test title
   * @description this is test desc
   * @default multiple
   */
  test: string;

  /**
   * @title this is aaa title
   * @description this is aaa desc
   * @default true
   */
  aaa: boolean;

  /** @default 1 */
  bbb: number;
}

The generated JSON is then transformed into a Formily‑compatible schema.

const json = {
  "Test": {
    "type": "object",
    "properties": {
      "test": { "type": "string", "description": "this is test desc", "title": "this is test title", "default": "multiple" },
      "aaa": { "type": "boolean", "description": "this is aaa desc", "title": "this is aaa title", "default": true },
      "bbb": { "type": "number", "default": 1 }
    }
  }
};

const schema = {
  name: "Test",
  properties: {
    test: { default: "multiple", title: "this is test title", type: "string", "x-component": "Input", "x-decorator": "FormItem", "x-decorator-props": { tooltip: "this is test desc" } },
    aaa: { default: true, title: "this is aaa title", type: "boolean", "x-component": "Switch", "x-decorator": "FormItem", "x-decorator-props": { tooltip: "this is aaa desc" } },
    bbb: { default: 1, type: "number", "x-component": "NumberPicker", "x-decorator": "FormItem" }
  }
};

Logic Linking

Static schemas are insufficient; we need to inject logic. Formily provides two mechanisms: SchemaReactions for simple field dependencies and Effects for complex business logic.

Field Linking (SchemaReactions)

We use dependency‑based reactions and expose only common properties to keep the UI simple. Example: show an image when either select1 or select2 equals “是”.

// initial schema
{
  "type": "object",
  "properties": {
    "select1": { "title": "选项一", "type": "string", "x-decorator": "FormItem", "x-component": "Radio.Group", "enum": [{"label":"是","value":"是"},{"label":"否","value":"否"}] },
    "select2": { "title": "选项二", "type": "string", "x-decorator": "FormItem", "x-component": "Radio.Group", "enum": [{"label":"是","value":"是"},{"label":"否","value":"否"}] },
    "image": { "type": "string", "x-component": "Image" }
  }
}

The generated reactions configuration looks like:

const reactions = {
  image: {
    display: {
      condition: 'or',
      target: 'visible',
      reactions: [
        { dependence: 'select1', attr: 'value', relation: '=', value: '是' },
        { dependence: 'select2', attr: 'value', relation: '=', value: '是' }
      ]
    }
  }
};

Converted back to Formily schema:

{
  "type": "object",
  "properties": {
    "select1": { /* ... */ },
    "select2": { /* ... */ },
    "image": {
      "type": "string",
      "x-component": "Image",
      "x-reactions": {
        "fulfill": { "state": { "display": "{{($deps[0] === '是' || $deps[1] === '是') ? 'visible' : 'none'}}" } },
        "dependencies": ["select1", "select2"]
      }
    }
  }
}

Business Logic Linking (Effects)

For workflow‑type forms we use Effects to run hook functions at different nodes. Example: hide select1 and select2 at node A.

function __execute() {
  formilyCoreApi.onFormMount(() => {
    const fields = form.query('*(select1, select2)');
    fields.forEach(field => field.setDisplay('none'));
  });
}
__execute();

We expose such actions as plugins with a JSON schema for configuration and an effects function that performs the operation.

const hookPlugin = {
  name: 'setNodesHidden',
  title: '字段隐藏设置',
  description: '可选择需要的字段在不同流程节点进行隐藏',
  version: '1.0.0',
  jsonSchema: {
    "type": "object",
    "properties": {
      "schemaKeys": {
        "type": "array",
        "x-decorator": "FormItem",
        "x-component": "SelectTable",
        "required": true,
        "x-component-props": { "bordered": false, "mode": "multiple" },
        "enum": [],
        "properties": {
          "label": { "title": "需要隐藏的字段名", "type": "string", "x-component": "SelectTable.Column" }
        }
      }
    }
  },
  effects: function(jsonSchema, formilyCoreApi, nodeId, formData) {
    formilyCoreApi.onFormMount(f => {
      f.setValues({ schemaKeys: formData || [] });
      const data = Object.keys(jsonSchema.properties).map(k => {
        const { title, customKey } = jsonSchema.properties[k];
        return { label: title || customKey, key: k };
      });
      const schemaKeysField = f.query('schemaKeys').take();
      schemaKeysField?.setDataSource(data);
    });
    formilyCoreApi.onFormSubmitValidateEnd(f => { f.values = f.values.schemaKeys; });
  },
  hook: function setNodesHidden(keys, global) {
    const { formilyCoreApi, form } = global;
    formilyCoreApi.onFormMount(() => {
      const fields = form.query(`*(${keys.join(',')})`);
      fields.forEach(field => field.setDisplay('none'));
    });
  }
};

Conclusion

After three months of operation, the platform has eliminated most ad‑hoc form requests from operations, freeing developers to focus on other business needs, although it reduced direct interaction with operations staff.

Frontend ArchitectureFormilyvisual editorform builder
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.