react hooks 实现动态表单存储

栏目: 服务器 · 发布时间: 5年前

内容简介:起点!这是我在掘金发布的第一篇文章,希望各位多多支持,如有错误,请不吝赐教,谢谢!---------------------------------------------这是一条分割线---------------------------------------------在上个月里, 公司承接了一个项目, 由我主编前端代码. 项目需求是实现一个服务类应用, 主要有以下功能:
react hooks 实现动态表单存储

起点!这是我在掘金发布的第一篇文章,希望各位多多支持,如有错误,请不吝赐教,谢谢!

---------------------------------------------这是一条分割线---------------------------------------------

背景

在上个月里, 公司承接了一个项目, 由我主编前端代码. 项目需求是实现一个服务类应用, 主要有以下功能:

  • 公示列表
  • 申请提交表单
  • 查看详情

看似挺简单的一些需求, 以为写起来不会很难, 于是乎就屁颠屁颠的开始搭建框架, 用公司前端前辈造好的轮子, 基于 react 开发的 @Gdjiami/cli 初始化项目, 简单的项目搭建好后, 就开始页面的开发了. 但是, 在开发的过程中遇到了以下问题:

  • 需求不明确:表单项数据结构会变化,渲染样式会变化
  • 表单的展示方式多样:不单单是简单的输入框, 而是有各种类型的输入框, 下拉选择, 弹窗选择, checkBoxGroup, 文件上传等
  • 多平台:项目需求在多种平台上展示,所以要做兼容移动端与 pc 端
  • 多种展示方式:表单的填写,表单的预览

这就是一个非常头疼的地方, 这也是整个项目开发中所遇到的难点, 接着在开发探索中找到了 动态表单 的运用.

react hooks 实现动态表单存储

那动态表单是什么呢? 我们应该如何去实现动态表单呢?

这就是我接下来想要分享的内容.

开发过程

动态表单是什么

动态表单就是需要能够通过一个配置文件去动态配置实现表单的渲染,并解决多平台展示,多种方式展示表单,灵活变动表单配置.

需求的确认总是必要的, 只是项目前期, 总体需求只能说大概出了有百分之八十, 剩下的都由后期完善, 所以我们也需要应对需求的变动做出灵活的开发.

当做到提交表单的填写页面的时候, 我缺少了一个整体组件库规划的过程. 这个过程其实能够让开发人员先预热下项目整体的组件框架设计, 这好比建房子, 如果在盖房之前, 没有将房子的整体框架设计好, 那你就不知道你要设计成什么样的房子, 没有这个框架, 那这个房子终究会被推倒重盖. 所以在开发初期, 必须设计好前端的组件框架, 以及设计出一个坚实, 可扩展性, 灵活性的父容器来支撑. 这里使用了 react 的上下文Context 来实现表单的存储、联动及验证.

这个项目的表单填写是分多页与多步骤提交的, 所以在填写完一页的表单后, 没有提交后台, 而是前端存储填写数据, 在点击下一步时进行数据验证, 而且有些表单项需要根据其他表单项进行联动.这时使用 context 定义一个 formStore 来存储、验证表单及实现表单的联动.

动态表单如何实现

定义 store 存储

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>
        )
      })}
    </>
  )
}
复制代码

FormPreviewerFormRenderer 基本相似, FormRenderer 负责填写表单时的组件渲染, FormPreviewer 负责表单填写完成后查看详情数据的组件渲染. 基本上动态表单的渲染及数据存储功能已完成, 后期如果需要新增数据项或者改动数据项格式, 以及改动组件样式, 都只是组件级的改动, 无需大改动.

总结

react hooks 实现动态表单存储

总结一下整个运用过程:可以先创建中心控制器 formStore 来控制表单的数据存储以及输出,接着创建表单项组件,并以 type 类型区分,然后创建表单渲染器,将每个表单项组件渲染到页面。 亦或者先将单个表单项组件创建后,再创建 formStore 来建立每个组件间的联系与数据存储.

原创编写,转载请注明出处,希望各位多多支持,谢谢!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

首席产品官1 从新手到行家

首席产品官1 从新手到行家

车马 / 机械工业出版社 / 2018-9-25 / 79

《首席产品官》共2册,旨在为产品新人成长为产品行家,产品白领成长为产品金领,最后成长为首席产品官(CPO)提供产品认知、能力体系、成长方法三个维度的全方位指导。 作者在互联网领域从业近20年,是中国早期的互联网产品经理,曾是周鸿祎旗下“3721”的产品经理,担任CPO和CEO多年。作者将自己多年来的产品经验体系化,锤炼出了“产品人的能力杠铃模型”(简称“杠铃模型”),简洁、直观、兼容性好、实......一起来看看 《首席产品官1 从新手到行家》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具