react hooks 實現動態表單存儲

6a5aaaeegy1fm42dxsp72j21hc0u0x6p

起點!這是我在掘金髮布的第一篇文章,但願各位多多支持,若有錯誤,請不吝賜教,謝謝!html

---------------------------------------------這是一條分割線---------------------------------------------前端

背景

在上個月裏, 公司承接了一個項目, 由我主編前端代碼. 項目需求是實現一個服務類應用, 主要有如下功能:react

  • 公示列表
  • 申請提交表單
  • 查看詳情

看似挺簡單的一些需求, 覺得寫起來不會很難, 因而乎就屁顛屁顛的開始搭建框架, 用公司前端前輩造好的輪子, 基於 react 開發的 @Gdjiami/cli 初始化項目, 簡單的項目搭建好後, 就開始頁面的開發了. 可是, 在開發的過程當中遇到了如下問題:git

  • 需求不明確:表單項數據結構會變化,渲染樣式會變化
  • 表單的展現方式多樣:不僅僅是簡單的輸入框, 而是有各類類型的輸入框, 下拉選擇, 彈窗選擇, checkBoxGroup, 文件上傳等
  • 多平臺:項目需求在多種平臺上展現,因此要作兼容移動端與 pc 端
  • 多種展現方式:表單的填寫,表單的預覽

這就是一個很是頭疼的地方, 這也是整個項目開發中所遇到的難點, 接着在開發探索中找到了動態表單的運用.github

image

那動態表單是什麼呢? 咱們應該如何去實現動態表單呢?數據結構

這就是我接下來想要分享的內容.框架

開發過程

動態表單是什麼

動態表單就是須要可以經過一個配置文件去動態配置實現表單的渲染,並解決多平臺展現,多種方式展現表單,靈活變更表單配置.ide

需求的確認老是必要的, 只是項目前期, 整體需求只能說大概出了有百分之八十, 剩下的都由後期完善, 因此咱們也須要應對需求的變更作出靈活的開發.oop

當作到提交表單的填寫頁面的時候, 我缺乏了一個總體組件庫規劃的過程. 這個過程其實可以讓開發人員先預熱下項目總體的組件框架設計, 這比如建房子, 若是在蓋房以前, 沒有將房子的總體框架設計好, 那你就不知道你要設計成什麼樣的房子, 沒有這個框架, 那這個房子終究會被推倒重蓋. 因此在開發初期, 必須設計好前端的組件框架, 以及設計出一個堅實, 可擴展性, 靈活性的父容器來支撐. 這裏使用了 react 的上下文 Context 來實現表單的存儲、聯動及驗證.spa

這個項目的表單填寫是分多頁與多步驟提交的, 因此在填寫完一頁的表單後, 沒有提交後臺, 而是前端存儲填寫數據, 在點擊下一步時進行數據驗證, 並且有些表單項須要根據其餘表單項進行聯動.這時使用 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 負責表單填寫完成後查看詳情數據的組件渲染. 基本上動態表單的渲染及數據存儲功能已完成, 後期若是須要新增數據項或者改動數據項格式, 以及改動組件樣式, 都只是組件級的改動, 無需大改動.

總結

image

總結一下整個運用過程:能夠先建立中心控制器 formStore 來控制表單的數據存儲以及輸出,接着建立表單項組件,並以 type 類型區分,而後建立表單渲染器,將每一個表單項組件渲染到頁面。 亦或者先將單個表單項組件建立後,再建立 formStore 來創建每一個組件間的聯繫與數據存儲.

原創編寫,轉載請註明出處,但願各位多多支持,謝謝!

相關文章
相關標籤/搜索