起点!这是我在掘金发布的第一篇文章,但愿各位多多支持,若有错误,请不吝赐教,谢谢!html
---------------------------------------------这是一条分割线---------------------------------------------前端
在上个月里, 公司承接了一个项目, 由我主编前端代码. 项目需求是实现一个服务类应用, 主要有如下功能:react
看似挺简单的一些需求, 觉得写起来不会很难, 因而乎就屁颠屁颠的开始搭建框架, 用公司前端前辈造好的轮子, 基于 react 开发的 @Gdjiami/cli 初始化项目, 简单的项目搭建好后, 就开始页面的开发了. 可是, 在开发的过程当中遇到了如下问题:git
这就是一个很是头疼的地方, 这也是整个项目开发中所遇到的难点, 接着在开发探索中找到了动态表单
的运用.github
那动态表单是什么呢? 咱们应该如何去实现动态表单呢?数据结构
这就是我接下来想要分享的内容.框架
动态表单就是须要可以经过一个配置文件去动态配置实现表单的渲染,并解决多平台展现,多种方式展现表单,灵活变更表单配置.ide
需求的确认老是必要的, 只是项目前期, 整体需求只能说大概出了有百分之八十, 剩下的都由后期完善, 因此咱们也须要应对需求的变更作出灵活的开发.oop
当作到提交表单的填写页面的时候, 我缺乏了一个总体组件库规划的过程. 这个过程其实可以让开发人员先预热下项目总体的组件框架设计, 这比如建房子, 若是在盖房以前, 没有将房子的总体框架设计好, 那你就不知道你要设计成什么样的房子, 没有这个框架, 那这个房子终究会被推倒重盖. 因此在开发初期, 必须设计好前端的组件框架, 以及设计出一个坚实, 可扩展性, 灵活性的父容器来支撑. 这里使用了 react 的上下文 Context 来实现表单的存储、联动及验证.spa
这个项目的表单填写是分多页与多步骤提交的, 因此在填写完一页的表单后, 没有提交后台, 而是前端存储填写数据, 在点击下一步时进行数据验证, 并且有些表单项须要根据其余表单项进行联动.这时使用 context 定义一个 formStore 来存储、验证表单及实现表单的联动.
store 就至关于一个数据中心控制器, 控制表单数据的输入与输出.
建立 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,
});
复制代码
定义 Store 类型为 key value 对象, 经过 setStore 和 getStore 对 form 数据进行读写操做, 存入 store . 定义 clear 方法进行数据清除. 定义 validate 方法, 经过传参定义判断规则, 进行数据验证. ( noop 定义类型且初始化方法, noop: () => any = () => {}
)
定义一个 FC (Function Component) 来渲染 Context.Provider
export interface FormStoreProps {
defaultValue?: object;
onChange?: (value: objext) => void;
}
export const FormStore: FC<FormStoreProps> = props => {
// 须要定义 store, setStore, getStore, validate, clear
return (
<Context.Provider value={{ store, setStore, getStore, validate, clear }}>
{props.children}
</Context.Provider>
);
};
复制代码
亦或者是能够直接使用已定义的 Context
export function useForm() {
return useContext(Context);
}
export function useFormStore() {
return useForm().store;
}
复制代码
使用这个上下文存储表单数据, 主要是由于须要在多个页面上渲染使用, 在填写表单页面时写入 store , 在填写完后查看填写详情页面中, 须要将以前写入的数据读取出并渲染在页面上展现.
首先将定义的 formStore 挂载在路由上
<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>
复制代码
这样的话就能够在填写表单页面和查看详情页面上共享 formStore 存储数据 const form = useForm()
.
接着, 如何实现多种表单填写方式呢?有普通的输入框, 下拉选择框, 文件上传, 地址选择等.在多个页面上, 而且一个页面有多个表单项.这里就用到动态表单斤进行配置.
每种填写表单项都是一个独立的组件, 输入框就定义了 InputItem 组件, 下拉选择框就定义了 Selector 组件, 文件上传就定义了 FileUploader 组件等.每一个组件并非经过单纯的导入, 而是经过动态配置进行渲染.
定义一个 useFormItem 方法, 返回其 value, onChange, 实现多组件统一读写数据.
/**
* @param name 表单项字段
* @param defaultValue 表单项默认值
* @param normalize 将值转换为表单能够接受的格式
* @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 };
}
复制代码
定义表单项配置属性 CommonFormOptions
, 每一个表单项遵循基础的表单项配置并进行拓展配置.
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
...
复制代码
FormRenderer
父容器所接收传入的配置项, 遍历配置项的各个元素后经过渲染器 return
节点元素. 在每一个组件配置都有 type 属性, 经过配置文件中配置表单项的 type 值, 再由动态表单的渲染器动态选择渲染组件.
// key 值为每一个组件的 type 属性值, 必须保持一致
const FormItemMap = {
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)) || {}
// 在这里能够对store进行操做, 如监听某字段变化, 改变其余字段的值;
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>
)
})}
</>
)
}
复制代码
FormPreviewer
与 FormRenderer
基本类似, FormRenderer
负责填写表单时的组件渲染, FormPreviewer
负责表单填写完成后查看详情数据的组件渲染. 基本上动态表单的渲染及数据存储功能已完成, 后期若是须要新增数据项或者改动数据项格式, 以及改动组件样式, 都只是组件级的改动, 无需大改动.
总结一下整个运用过程:能够先建立中心控制器 formStore
来控制表单的数据存储以及输出,接着建立表单项组件,并以 type
类型区分,而后建立表单渲染器,将每一个表单项组件渲染到页面。 亦或者先将单个表单项组件建立后,再建立 formStore
来创建每一个组件间的联系与数据存储.
原创编写,转载请注明出处,但愿各位多多支持,谢谢!