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.
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.
Goodme Frontend Team
Regularly sharing the team's insights and expertise in the frontend field
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
