Background#
Due to work reasons, I switched to a completely unfamiliar tech stack to develop a new project for nearly a month. React and Storybook were manageable, as I got the hang of them in about a day, but it took me over half a month to get started with Formily. Now that a phase of work has ended, I’ll summarize these experiences.
Due to project reasons, my use of Formily may not be the best practice and might not even be correct. My usage related to forms is almost zero, with the main scenarios focused on using @formily/antd-v5 and custom components.
Basics#
Formily is an open-source framework developed by Alibaba that describes forms using Schema. However, the reason we use it in our project is not for building forms, but for dynamically constructing pages.
There are three important concepts in Formily: Form, Field, and Schema.
- Form is the core of the entire form, and its instance can be used to get and set form validation, values, fields, field linkage, and other content.
- Field is a component of the Form, representing fields. For example, if a form has an input box named
keyword
, then every time the content of the input box is modified,form.values.keyword
will change accordingly. - Schema describes the data of the Field and can be simply considered equivalent to ReactNode. For specific contents that a Schema can include, refer to the official documentation.
- During the process of converting Schema to components, Formily will by default proxy the component's
value
andonChange
, so this needs to be considered when developing Formily components.
- During the process of converting Schema to components, Formily will by default proxy the component's
Starting with a Simple Page#
Let's start with a simple example.
The code above creates a scene of a ==“div with a border containing an input box and another container with two input boxes”==. From the code, we can draw several conclusions:
- The hierarchical structure of Schema corresponds to the hierarchical structure in HTML, where
properties
serves a role similar tochildren
. - Can some initial states of the form be defined in
createForm
? x-component
represents the element that needs to be rendered at the current position, supporting both native tags and custom components, with custom components needing to be registered increateSchemaField
.x-component-props
defines the props of the component, whilex-decorator
defines the component that wraps this component.
By performing simple operations on the form, we can observe the mapping relationship between Schema and the actual form values.
If we want to manage the values of different fields, the type
in the schema is very crucial.
- When
type
isvoid
, the form will ignore the path at this level. - When
type
isobject
, this field will become an object that holds child fields. - When
type
is a basic type, this field represents a specific value.
At that time, we originally planned to encapsulate a component that combined Card + Tabs functionality, but under Formily's mechanism, a field itself cannot represent its own value (Tabs' activeKey) and also become an object containing child fields, so we ultimately abandoned this approach.
Function Handling#
If we insist on using JSON Schema, then stuffing functions into the Schema seems less reasonable. In the Schema, Formily will process strings in {{}}
as functions, so we need to adjust the syntax.
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
onClick: `{{ (event) => { console.log(event) } }}`
}
}
// ...
Similarly, for props that need to pass ReactNode, the same processing applies, but we need to use React.createElement
.
import { createElement } from "react";
import { Input } from '@formily/antd-v5'
import { SearchOutlined } from '@ant-design/icons';
// ...
const SchemaField = createSchemaField({
components: {
Input
},
scope: {
createElement,
SearchOutlined
}
})
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
suffix: `{{ createElement(SearchOutlined) }}`
}
}
// ...
Field Linkage#
This part is well explained in the official documentation, so I won't elaborate further.
Data Transmission#
At that time, our page had a linkage logic where switching the time in the top navigation bar would require all the charts below to update their data synchronously. Our solution was as follows, which may not be the best practice.
Component Development Process#
Formily components differ significantly from regular React components. If we take @formily/antd-v5
as the officially recommended practice, we cannot develop components using previous approaches.
Taking antd's Table as an example, both columns
and dataSource
are passed as part of the props to the component. However, in Formily, data should only be handled by the default configuration of props.value
, so during development, we should ensure the conversion between rendering data and props.value
. When integrating existing components, we can also use the official mapProps method for mapping.
To be precise, data can be placed anywhere to achieve functionality, but being able to easily access all values from
form.values
is more convenient than scattering data into the field'scomponentProps
.
In addition, effectively using useForm
, useField
, and useFieldSchema
can facilitate the retrieval of parent and child field content.
Some Pitfalls#
ReactNode to Schema#
Although we can pass ReactNode using {{createElement}}
, for custom components, we still need to pass the component into the scope in advance, which is difficult to achieve with dynamic Schema.
My approach was to intercept within the component. I used useFieldSchema
to obtain the Schema, then checked whether the corresponding property was also a Schema object. If so, I returned a RecursionField
to render the Schema.
After writing the interception, it seemed fine at first glance, but I was baffled when it came to the sortIcon of antd's Table, as the rendered icon had no state change upon clicking.
I suspect this is related to Formily's rendering mechanism, where the rendering result of RecursionField
is cached, and simply rerunning the function with new props is insufficient to trigger a re-render.
onChange#
This is also related to the filtering of the Table. In the original process, clicking the header for sorting would trigger the table's onChange
event to obtain sorting information.
However, after trying various syntaxes, I found that this onChange
could still not be triggered. After looking at the source code, I discovered that to prevent bubbling, the official implementation had overridden onChange
with an empty function. Do you not use this functionality at all?
There was no business-level solution to this, so I ultimately resolved it using a patch, which was quite cumbersome.