@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
233 lines • 7.43 kB
JavaScript
import _isUndefined from "lodash/isUndefined";
import React, { Component } from 'react';
import { getUuidv4 } from '@douyinfe/semi-foundation/lib/es/utils/uuid';
import { FormUpdaterContext, ArrayFieldContext } from './context';
import warning from '@douyinfe/semi-foundation/lib/es/utils/warning';
import copy from 'fast-copy';
const filterArrayByIndex = (array, index) => array.filter((item, i) => i !== index);
const getUuidByArray = array => array.map(() => getUuidv4());
const getUpdateKey = arrayField => {
if (!arrayField) {
return undefined;
}
if (arrayField && arrayField.updateKey) {
return arrayField.updateKey;
}
return undefined;
};
const initValueAdapter = initValue => {
const iv = [];
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, oldKeys) => {
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 {
constructor(props, context) {
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 = copy(initValue);
const initValueCopyForReset = copy(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(index) {
const {
keys
} = this.state;
const {
field
} = this.props;
const updater = this.context;
const newKey = getUuidv4();
const opts = {
notNotify: true,
notUpdate: true
};
if (typeof index === 'number') {
const safeIndex = Math.max(0, Math.min(index, keys.length));
keys.splice(safeIndex, 0, newKey);
let currentValues = updater.getValue(field);
if (Array.isArray(currentValues)) {
currentValues = currentValues.slice();
currentValues.splice(safeIndex, 0, undefined);
updater.updateStateValue(field, currentValues, opts);
}
let currentErrors = updater.getError(field);
if (Array.isArray(currentErrors)) {
currentErrors = currentErrors.slice();
currentErrors.splice(safeIndex, 0, undefined);
updater.updateStateError(field, currentErrors, opts);
}
} else {
keys.push(newKey);
}
this.shouldUseInitValue = true;
this.setState({
keys
});
const updateKey = new Date().valueOf();
updater.updateArrayField(field, {
updateKey
});
this.cacheUpdateKey = updateKey;
return newKey;
}
addWithInitValue(rowVal, index) {
const updater = this.context;
const {
field
} = this.props;
const newArrayFieldVal = updater.getValue(field) ? updater.getValue(field).slice() : [];
const cloneRowVal = copy(rowVal);
if (typeof index === 'number') {
const safeIndex = Math.max(0, Math.min(index, newArrayFieldVal.length));
newArrayFieldVal.splice(safeIndex, 0, cloneRowVal);
} else {
newArrayFieldVal.push(cloneRowVal);
}
updater.updateStateValue(field, newArrayFieldVal, {});
updater.updateArrayField(field, {
updateKey: new Date().valueOf()
});
}
remove(i) {
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,
inArrayField: true
};
return /*#__PURE__*/React.createElement(ArrayFieldContext.Provider, {
value: contextVal
}, children({
arrayFields,
add,
addWithInitValue
}));
}
}
ArrayFieldComponent.contextType = FormUpdaterContext;
export default ArrayFieldComponent;