- Notifications
You must be signed in to change notification settings - Fork3
A meta based form builder for React.
License
eBay/nice-form-react
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
While there're already some form libraries/frameworks focus on state management or reuseful components, NiceForm mainly focus on form layout, dynamically show/hide form fields, switch view/edit modes, etc, in a meta based approach.
Also check out our another nice utility!nice-modal-react! 😜
It is a small utility with zero dependencies and can be seamlessly integrated with any React UI library or form state manager and comes with built-in support for native HTML,antd.design, andFormik +MUI.
Before read through the detailed introduction below, we suggest you first go through our examples page so that you get a quick glance at the scenarios NiceForm is used for and how API looks like:
First, install nice form as a dependency:
npm i @ebay/nice-form-react
Then, config NiceForm to use which UI library by adding adapter(s) to your app entry (usuallysrc/main.tsx
):
import{configasniceFormConfig}from'@ebay/nice-form-react';importformikAdapterfrom'@ebay/nice-form-react/adapters/formikAdapter';importformikMuiAdapterfrom'@ebay/nice-form-react/adapters/formikMuiAdapter';niceFormConfig.addAdapter(formikAdapter);niceFormConfig.addAdapter(formikMuiAdapter);
At last, use it to render your form:
import{Form,Formik,FormikProps}from'formik';importButtonfrom'@mui/material/Button';importNiceFormfrom'@ebay/nice-form-react';import{FormikMuiNiceFormMeta}from'@ebay/nice-form-react/adapters/formikMuiAdapter';constSimple=()=>{constinitialValues={username:'',password:'',};constgetMeta=(form:FormikProps<typeofinitialValues>)=>{constformMeta:FormikMuiNiceFormMeta={columns:1,rowGap:18, form, initialValues,disabled:form.isSubmitting,fields:[{key:'username',label:'User Name',widget:'text',},{key:'password',label:'Password',widget:'text',widgetProps:{type:'password',},},{key:'submit',render:()=>{return(<Buttontype="submit"variant="contained"> Log in</Button>);},},],};returnformMeta;};return(<divstyle={{width:'400px'}}><FormikinitialValues={initialValues}onSubmit={async(values)=>{awaitnewPromise((r)=>setTimeout(r,500));alert(JSON.stringify(values,null,2));}}>{(form)=>{return(<Form><NiceFormmeta={getMeta(form)}/></Form>);}}</Formik></div>);};
Here is the result:
- Small: ~3kb gzipped.
- Easy layout: grid based layout system.
- Meta based: easy to show/hide fields dynamically.
- View/edit modes: one meta to support both view and edit modes.
- Flexible: you can mix meta and original API to create forms.
- Out of box: built-in support for Mui and AntD.
- Extensible: easy to create custom adapters to support more UI libraries.
Understanding the workings of NiceForm is essential, as it paves the way for effortless usage across various scenarios. NiceForm's simplicity lies not in magic but in its mechanism that transforms meta objects into JSX APIs offered by corresponding form libraries. Consider the straightforward usage mentioned above as an example:
constMyComp=()=>{constformMeta={fields:[{key:'username',label:'User Name'},{key:'password',label:'Password',widget:'password'},],};return<NiceFormmeta={formMeta}/>;};
The logic just transforms meta to JSX:
constMyComp=()=>{return(<><FormItemkey="username"label="User Name"><Input/><FormItem><FormItemkey="password"label="Password"><Inputtype="password"/><FormItem></> );}
SoNiceForm
itself can be used anywhere, but usually you just put<NiceForm />
in some form context. Similarily, since it's just a helper, you can provide multipleNiceForm
tags in one form:
constmeta1={...}constmeta2={...}return(<Form><NiceFormmeta={meta1}/><SomeOtherContent...><NiceFormmeta={meta2}/></Form>);
Besides that, nice form puts fields into a grid layout system. So if necessary, a field rendered by nice form is wrapped with a wrapper to be layouted by css grid. For example, we can define the total columns of the form and specify how many columns a field takes:
constmeta={columns:4,columnGap:12,fields:[{key:'address2',label:'Address',colSpan:4},{key:'city',label:'City',colSpan:2},{key:'state',label:'State'},{key:'zip',label:'Zip Code'},]
Then you get the form like below:
Note that, a field meta is not necessary being a form field, but any UI element, for example, in the simple usage case, we define button with raw UI library, alternatively you can also define it with meta:
constmeta={columns:1,fields:[{key:'username',label:'User Name'},{key:'password',label:'Password',widget:'password'},// You can define a render method to render anything{key:'hr',label:null,render:()=><hr/>,},// Here you can define the button by meta, note a widget can be any react component{key:'btn',label:null,wrapperCol:{span:16,offset:8},widget:Button,widgetProps:{type:'primary',children:'Log in'},},],};return<NiceFormmeta={meta}/>;
The you get the UI like below:
NiceForm uses css grid to layout fields of a form. The related props include:
- FormMeta.columns: how many columns of the form, the default value is
1
. - FormField.colSpan: how many columns the field takes, for example, for FormMeta.columns is
3
, the fieldcolSpan
is2
then the field takes 2/3 row space. - FormField.clear: the value can be
left
,right
orboth
. It means if there can be a field shows in left, right or none. It will ignorecolSpan
value.
There are two approaches to define if a field should be showed.
For example, you need to show a text field based on the value of another field:
constmeta={fields:[{key:'favoriteFruit',label:'Favorite Fruit',widget:'radio-group',options:['Apple','Orange','Other'],initialValue:'Apple',},],};// Push a new field if choose othersif(NiceForm.getFieldValue('favoriteFruit',meta,form)==='Other'){meta.fields.push({key:'otherFruit',label:'Other',});}
You can also check the live examplehere.
NiceForm supportscondition
property on field to decide if the field should be rendered. It's a boolean value or a function returns a boolean value:
boolean|functioncondition({meta:meta,field:field}){}
For example, besides approach 1 we can usecondition
alternatively:
constmeta={fields:[{key:'favoriteFruit',label:'Favorite Fruit',widget:'radio-group',options:['Apple','Orange','Other'],initialValue:'Apple',condition:()=>NiceForm.getFieldValue('favoriteFruit',meta,form)==='Other'},],};
Also, see the live examplehere.
NiceForm provides two modes for form: edit mode and view mode. So you usually can render two modes of a form with a single meta object. For each field, you can define how a field is rendered under edit mode or view mode.
The mode could be form level or field level. To understand how it works, just see the examplehere.
You can see the full API referencehere.
To create a form with Nice Form, you usually use below APIs:
- NiceForm component: a holder component to render form by meta. Always:
<NiceForm meta={meta} />
- NiceFormMeta: the structure to define the form meta.
- NiceFormField: the element type of
fields
property ofNiceFormMeta
. Used to define a form field.
Note that,NiceFormMeta
andNiceFormField
are extensible. Different adapters usually provide their own meta type (inheriting the base) to provide customized/enhanced behaviors.
For built-in adapters, you can see meta types in
- AntdFormMeta
- AntdFormFieldMeta
- MuiFormMeta
- MuiFormFieldMeta
Here is the common meta properties:
exportinterfaceNiceFormMetaextendsRecord<string,any>{/** * If you have mulitple forms in one page, you may set different names so that fields ids will not conflict. */name?:string;// used as prefix for form field id/** * How many columns in the grid layout system. */columns?:number;/** * The label width of the field. It differs when using different adapters. * For example, you can use 1-24 in antd, but it's unncessary in MUI. */labelWidth?:number|string;// 1-24/** * The column gap of the grid layout system. */columnGap?:string|number;/** * The row gap of the grid layout system. */rowGap?:string|number;/** * Whether the form is disabled (means all fields are disabled) */disabled?:boolean;/** * ? */layout?:any;/** * Whether the form is in view mode. If true, all fields will be rendered with viewWidget. */viewMode?:boolean;/** * The default widget component or widget id for fields in edit mode. This overrides the defaultWidget in config. */defaultWidget?:string|ReactComponent|null;/** * The default widget component or widget id for fields in view mode. This overrides the defaultViewWidget in config. */defaultViewWidget?:string|ReactComponent|null;/** * Deprecated. */wrapperProps?:Record<string,any>;/** * The initial values of the form. */initialValues?:Record<string,any>;/** * The fields of the form. */fields:NiceFormField[];}
exportinterfaceNiceFormFieldextendsRecord<string,unknown>{/** * The field key. It's used to identify the field. It's used as the field name if the name * is not defined. In this case, the name is generated by splitting the key by '.'. * For example: if the key is 'user.name', the name will be ['user', 'name']. * So it's convenient to support nested object. * * Note: if you want a dot `.` to be part of the field name, you should specify the name explicitly. * For example: name: ['user', 'name.with.dot'] */key:string;/** * The name of the field, its format may be different for different adapters. * For example, in antd, it's an array of path. See API doc of the library you use. */name?:string|Array<string>;/** * The label of the field, adapters should implement the support for labels. */label?:ReactNode;/** * Help information of the field, consumed by adapter. */help?:ReactNode;/** * Whether the field is required. */required?:boolean;/** * Whether the field is disabled. */disabled?:boolean;/** * Whether the field is full width of the space. */fullWidth?:boolean;/** * The initial value of the field. */initialValue?:any;/** * The extra node of the field. Appended just after the field component. * You usually need to use css to position it. */extraNode?:ReactNode;/** * The label width. */labelWidth?:string;/** * Only for field widget which consumes a list data. * For example: select, checkbox group, radio group, etc. */options?:any[];/** * Nice form provides the grid layout system, colSpan is used to specify how many * columns of the field. */colSpan?:number;/** * Nice form provides the grid layout system, colSpan is used to specify how many * rows of the field. */rowSpan?:number;/** * If the field is in view mode, it will use viewWidget to render the field. * Note that this is different with readOnly property which passes readOnly to the * field component. */viewMode?:boolean;/** * If the field widget accepts the children, then you can specify it by this. */children?:ReactNode|Function;/** * In the grid layout system, if the field allows other fields in `left`, `right` or `both` * position. For example, if clear is `right`, then no other fields in the `right` even * there's still space (by columns). */clear?:'left'|'right'|'both';/** * You can use `render` method to render any content of the field in both edit and view mode. It will replaces * the logic provided by adapters. You need to handle the edit/view mode in the function. *@param param0 *@returns */render?:({ field, meta}:{field:NiceFormField;meta:NiceFormMeta})=>ReactElement|null;/** * You can use `renderView` method to render the widget (without label). *@param value *@param param1 *@returns */renderView?:(value:any,{ field, meta}:{field:NiceFormField;meta:NiceFormMeta},)=>ReactElement|null;/** * The widget type. */widgetType?:string|null;/** * The widget component or widget id in edit mode. */widget?:string|ReactComponent|null;/** * The edit mode widget component props. */widgetProps?:NiceFormWidgetProps;/** * The widget component or widget id in view mode. */viewWidget?:string|ReactComponent|null;/** * The view mode widget component props. */viewWidgetProps?:Record<string,any>;/** * A function returns true or false to determine if the field should be rendered. * It's convenient to show/hide fields dynamically. * * If you need complex logic, you can modify meta fields directlly after meta is constructed. *@param param0 *@returns */condition?:({ field, meta}:{field:NiceFormField;meta:NiceFormMeta})=>boolean;}
If you were usingantd-form-builder, you may notice below API changes:
- elements -> fields
- no
form
prop onNiceForm
- NiceForm.useUpdateOnChange(fields)
- Now
initialValues
has higher priority than the original - New
condition
callback. Return false if the filed should be hidden. - Support shouldUpdate, dependencies
- Use
Form.useWatch
for most dynamic cases.
We use pnpm workspace to manage packages, so just run below command under root folder. It will install deps for all packages.
pnpm install
cd packages/nice-form-reactpnpm dev
We provide antd and formik examples, start them.
cd packages/examples-[antd|formik]pnpm link ../nice-form-reactpnpm dev
Then accesshttp://localhost:5173 or 5174 as the console indicated.
MIT
About
A meta based form builder for React.