123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- /* eslint-disable react/destructuring-assignment */
- import React, { Component } from 'react';
- import { getUuidv4 } from '@douyinfe/semi-foundation/utils/uuid';
- import { cloneDeep, isUndefined } from 'lodash';
- import { FormUpdaterContext, ArrayFieldContext } from './context';
- import warning from '@douyinfe/semi-foundation/utils/warning';
- import { ArrayFieldStaff, FormUpdaterContextType } from '@douyinfe/semi-foundation/form/interface';
- export interface ArrayFieldProps {
- initValue?: any[];
- field?: string;
- children?: (props: ArrayFieldChildrenProps) => React.ReactNode;
- }
- export interface ArrayFieldChildrenProps {
- arrayFields: {
- key: string;
- field: string;
- remove: () => void;
- }[];
- add: () => void;
- addWithInitValue: (lineObject: Record<string, any>) => void;
- }
- export interface ArrayFieldState {
- keys: string[];
- }
- const filterArrayByIndex = (array: any[], index: number) => array.filter((item, i) => i !== index);
- const getUuidByArray = (array: any[]) => array.map(() => getUuidv4());
- const getUpdateKey = (arrayField: ArrayFieldStaff): string | undefined => {
- if (!arrayField) {
- return undefined;
- }
- if (arrayField && arrayField.updateKey) {
- return arrayField.updateKey;
- }
- return undefined;
- };
- const initValueAdapter = (initValue: any) => {
- const iv: any[] = [];
- if (Array.isArray(initValue)) {
- return initValue;
- } else {
- warning(
- !isUndefined(initValue),
- '[Semi Form ArrayField] initValue of ArrayField must be an array. Please check the type of your props'
- );
- return iv;
- }
- };
- /**
- *
- * @param {any[]} value
- * @param {string[]} oldKeys
- * @returns string[]
- */
- const generateKeys = (value: any[], oldKeys?: string[]) => {
- const val = initValueAdapter(value);
- const newKeys = getUuidByArray(val);
- // return newKeys;
- const keys = newKeys.map((key, i) => (oldKeys && oldKeys[i] ? oldKeys[i] : key));
- return keys;
- };
- class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
- static contextType = FormUpdaterContext;
- cacheFieldValues: any[];
- shouldUseInitValue: boolean;
- cacheUpdateKey: string;
- context: FormUpdaterContextType;
- constructor(props: ArrayFieldProps, context: FormUpdaterContextType) {
- super(props, context);
- const initValueInProps = this.props.initValue;
- const { field } = this.props;
- const initValueInForm = context.getValue(field);
- const initValue = initValueInProps || initValueInForm;
- this.state = {
- keys: generateKeys(initValue),
- };
- this.add = this.add.bind(this);
- this.addWithInitValue = this.addWithInitValue.bind(this);
- this.remove = this.remove.bind(this);
- this.cacheFieldValues = null;
- this.cacheUpdateKey = null;
- /*
- If updateKey exists, it means that the arrayField (usually a nested ArrayField not at the first level) is only re-mounted due to setValues,
- and the fields it contains do not need to consume initValue
- */
- // whether the fields inside arrayField should use props.initValue in current render process
- this.shouldUseInitValue = !context.getArrayField(field);
- // Separate the arrays that reset and the usual add and remove modify, otherwise they will affect each other
- const initValueCopyForFormState = cloneDeep(initValue);
- const initValueCopyForReset = cloneDeep(initValue);
- context.registerArrayField(field, initValueCopyForReset);
- // register ArrayField will update state.updateKey to render, So there is no need to execute forceUpdate here
- context.updateStateValue(field, initValueCopyForFormState, { notNotify: true, notUpdate: true });
- }
- componentWillUnmount() {
- const updater = this.context;
- const { field } = this.props;
- updater.unRegisterArrayField(field);
- }
- componentDidUpdate() {
- const updater = this.context;
- const { field } = this.props;
- const { keys } = this.state;
- const fieldValues = updater.getValue(field);
- const updateKey = getUpdateKey(updater.getArrayField(field));
- // when update form outside, like use formApi.setValue('field', [{newItem1, newItem2}]), formApi.setValues
- // re generate keys to update arrayField;
- if (updateKey !== this.cacheUpdateKey) {
- const newKeys = generateKeys(fieldValues, keys);
- // eslint-disable-next-line
- this.setState({ keys: newKeys });
- this.cacheUpdateKey = updateKey;
- if (this.cacheUpdateKey !== null) {
- this.shouldUseInitValue = false;
- }
- }
- }
- add() {
- const { keys } = this.state;
- keys.push(getUuidv4());
- this.shouldUseInitValue = true;
- this.setState({ keys });
- }
- addWithInitValue(lineObject: Record<string, any>) {
- const updater = this.context;
- const { field } = this.props;
- const newArrayFieldVal = updater.getValue(field) ? updater.getValue(field).slice() : [];
- newArrayFieldVal.push(lineObject);
- updater.updateStateValue(field, newArrayFieldVal, {});
- updater.updateArrayField(field, { updateKey: new Date().valueOf() });
- }
- remove(i: number) {
- const updater = this.context;
- const { keys } = this.state;
- const { field } = this.props;
- const newKeys = filterArrayByIndex(keys, i);
- // Make sure that all the keys in the line are removed, because some keys are not taken over by the field, only set in the initValue
- let newArrayFieldError = updater.getError(field);
- const opts = { notNotify: true, notUpdate: true };
- if (Array.isArray(newArrayFieldError)) {
- newArrayFieldError = newArrayFieldError.slice();
- newArrayFieldError.splice(i, 1);
- updater.updateStateError(field, newArrayFieldError, opts);
- }
- // if (Array.isArray(newArrayFieldTouched)) {
- // newArrayFieldTouched = newArrayFieldTouched.slice();
- // newArrayFieldTouched.splice(i, 1);
- // updater.updateStateTouched(field, newArrayFieldTouched, opts);
- // }
- let newArrayFieldValue = updater.getValue(field);
- if (Array.isArray(newArrayFieldValue)) {
- newArrayFieldValue = newArrayFieldValue.slice();
- newArrayFieldValue.splice(i, 1);
- updater.updateStateValue(field, newArrayFieldValue);
- }
- this.setState({ keys: newKeys });
- }
- render() {
- const { children, field } = this.props;
- const { keys } = this.state;
- const arrayFields = keys.map((key, i) => ({
- // key: i,
- key,
- field: `${field}[${i}]`,
- remove: () => this.remove(i),
- }));
- const { add } = this;
- const { addWithInitValue } = this;
- const contextVal = {
- shouldUseInitValue: this.shouldUseInitValue,
- };
- return (
- <ArrayFieldContext.Provider value={contextVal}>
- {children({ arrayFields, add, addWithInitValue })}
- </ArrayFieldContext.Provider>
- );
- }
- }
- export default ArrayFieldComponent;
|