The starting point! This is my first article published in nuggets, hope you support, if there is any mistake, please feel free to comment, thank you!

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — this is the line — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — –

background

Last month, the company undertook a project and I was the chief editor of the front-end code. The requirement of the project is to realize a service application, which has the following functions:

  • The public list
  • Application Submission Form
  • Check the details

For some seemingly simple requirements, I thought it would not be difficult to write, so I started to build a framework, using the wheels built by the company’s front-end predecessors, and initializing the project based on @gdjiami/CLI. After the simple project was built, the page development started. However, in the process of development encountered the following problems:

  • Requirements are unclear: form item data structures change, rendering styles change
  • Forms are presented in a variety of ways: not just simple input boxes, but various types of input boxes, drop-down selection, popover selection, checkBoxGroup, file upload, etc
  • Multi-platform: The project needs to be displayed on multiple platforms, so it should be compatible with mobile and PC terminals
  • Various display methods: form filling, form preview

This is a very headache, which is also the difficulty encountered in the whole project development, and then found the application of dynamic forms in the development exploration.

So what are dynamic forms? How do we implement dynamic forms?

That’s what I want to share next.

The development process

What are dynamic forms

Dynamic forms need to be able to dynamically configure forms through a configuration file to achieve form rendering, and solve the multi-platform display, a variety of ways to display forms, flexible change form configuration.

Confirmation of demand is always necessary, but in the early stage of the project, only about 80% of the overall demand can be identified, and the rest will be improved in the later stage. Therefore, we also need to make flexible development in response to changes in demand.

When it came to completing the submission form, I was missing an overall component library planning process. This process actually allows developers to preheat the design of the overall component frame of the project. This is like building a house. If you don’t design the overall frame of the house before building it, you don’t know what kind of house you are going to design. Therefore, in the early stage of development, it is necessary to design a good front-end component framework, and design a solid, extensible, flexible parent container to support. React Context is used to store, link, and authenticate forms.

The form filling in this project is divided into multi-pages and multi-steps, so after filling in the one-page form, the front end does not submit the background, but stores the filling data and performs data verification when clicking next. Moreover, some form items need to be linked according to other form items. Use context to define a formStore to store, validate, and associate forms.

How are dynamic forms implemented

Defining store storage

The Store acts as a data center controller that controls the input and output of form data.

To create the context

const Context = React.createContext<{ store: Store; setStore: React.Dispatch<React.SetStateAction<{}>>; getStore: (rules? : ValidateRules[]) => Store; clear: () => void; validate: (rules: ValidateRules[]) => Promise<void>; }>({ store: {}, setStore: noop, getStore: noop, validate: noop, clear: noop, });Copy the code

Define Store type as key value object. Use setStore and getStore to read and write form data and Store it in Store. Define the clear method to clear data. (noop: () => any = () => {}))

Define a FC (Function Component) to render context.provider

export interface FormStoreProps { defaultValue? : object; onChange? : (value: objext) => void; } export const FormStore: FC<FormStoreProps> = props => {setStore, getStore, validate, clear return ( <Context.Provider value={{ store, setStore, getStore, validate, clear }}> {props.children} </Context.Provider> ); };Copy the code

Or you can just use the defined Context

export function useForm() {
  return useContext(Context);
}
export function useFormStore() {
  return useForm().store;
}
Copy the code

This context is used to store form data mainly because it needs to be rendered on multiple pages. When filling in the form page, the data is written into store. When viewing the filling details page after filling in, the data written before needs to be read out and rendered and displayed on the page.

First mount the formStore defined on the route

<FormStore defaultValues={detail.value} onChange={setCurrentValues}> <Container> <Switch> <Route path="/new/preview/:id?" component={Preview} /> <Route path="/new/index/:id?" > <Steps onStepChange={handleStepChange} steps={steps} defaultStep={search.step && parseInt(search.step, 10)} onFinish={handleOk} /> </Route> </Switch> </Container> </FormStore>Copy the code

Const form = useForm(); const form = useForm();

Define the form renderer

Next, how do you implement multiple forms? There are ordinary input boxes, drop-down selection boxes, file upload, address selection and so on. On multiple pages, and one page has multiple form items. This is where dynamic forms are configured.

Each form entry is a separate component. The input box defines the InputItem component, the drop-down select box defines the Selector component, the file upload defines the FileUploader component, and so on. Each component is rendered not through pure import but through dynamic configuration.

Define a useFormItem method, return its value, onChange, realize multi-component unified read and write data.

/** * @param name form item field * @param defaultValue form item defaultValue * @param normalize converts the value to an acceptable form * @param transform Export function useFormItem<T>(name: string, defaultValue? : T, normalize: (src: T) => any = identity, transform: (value: any) => T = identity ) { const context = useContext(Context); const value = normalize(context.store[name]); UseEffect (() => {context.setStore(store => {if (! (name in store) && defaultValue ! = null) { return { ... store, [name]: defaultValue, }; } return store; }); } []); const onChange = useCallback((value? : T) => { context.setStore(store => { return { ... store, [name]: transform(value), }; }); } []); return { value, onChange }; }Copy the code

Define the form item configuration property CommonFormOptions. Each form item follows the base form item configuration and extends the configuration.

export interface CommonFormOptions { ... } export interface InputOption extends CommonFormOptions { type: 'input', ... } export interface NumberOption extends CommonFormOptions { type: 'number', ... } export interface TextareaOption extends CommonFormOptions { type: 'textarea', ... } export interface SelectOption extends CommonFormOptions { type: 'select', ... }... export type FormOption = | InputOption | NumberOption | TextareaOption | SelectOption ...Copy the code

FormRenderer A configuration item passed in by the parent container, which iterates through the elements of the configuration item and returns the node element through the renderer. In each component configuration, there is a Type attribute, which configures the type value of the form item in the configuration file, and dynamically selects the rendering component by the renderer of the dynamic form.

Const FormItemMap = {INPUT: InputItem, number: NumberInput, textarea: {input: InputItem, number: NumberInput, textarea: TextareaItem, select: SelectItem, ... }; const FormItemRenderer: FC<{option: FormOption}> = props => { const { type, component, name, defaultValue, normalize, transform, dynamic, ... other } = props.option const formProps = useFormItem(name, defaultValue, normalize, transform) const Component = type === 'custom' ? component : FormItemMap[type] > || FormItemMap.input const { store, getStore, SetStore} = useForm () const dynamicProps = (dynamic && dynamic (formProps. Value, store)) | | {} / / here is able to store, Such as monitoring the change of a field, change the value of other fields; return <>{show ? React.createElement(Component, { ... formProps, ... other, subItems, ... dynamicProps }) : undefined}</> } const FormRenderer: FC<{ fields: FormOption[][] }> = props => { return ( <> {props.fields.map((group, index) => { return ( <List key={index}> {group.map(i => ( <FormItemRenderer key={i.name} option={i} /> ))} </List> ) })} } < / a >)Copy the code

FormPreviewer is basically similar to FormRenderer in that FormPreviewer is responsible for rendering components when the form is filled in, and FormPreviewer is responsible for viewing detailed data after the form is filled in. Basically, the rendering and data storage functions of dynamic forms have been completed. If new data items or changes of data item formats or component styles are needed in the later stage, they are component-level changes without major changes.

conclusion

To summarize the process, you can first create a central controller, formStore, to control the data storage and output of the form, then create form item components, separated by type type, and then create a form renderer to render each form item component to the page. Or you can create a single form item component first and then create a formStore to establish the connection and data store between each component.

Original preparation, reproduced please indicate the source, hope you a lot of support, thank you!