@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.
533 lines (531 loc) • 22.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _classnames = _interopRequireDefault(require("classnames"));
var _constants = require("@douyinfe/semi-foundation/lib/cjs/form/constants");
var _utils = require("@douyinfe/semi-foundation/lib/cjs/form/utils");
var ObjectUtil = _interopRequireWildcard(require("@douyinfe/semi-foundation/lib/cjs/utils/object"));
var _isPromise = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/isPromise"));
var _warning = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/warning"));
var _index = require("../hooks/index");
var _errorMessage = _interopRequireDefault(require("../errorMessage"));
var _reactUtils = require("../../_base/reactUtils");
var _label = _interopRequireDefault(require("../label"));
var _grid = require("../../grid");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/* eslint-disable react-hooks/rules-of-hooks */
const prefix = _constants.cssClasses.PREFIX;
// To avoid useLayoutEffect warning when ssr, refer: https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
// Fix issue 1140
const useIsomorphicEffect = typeof window !== 'undefined' ? _react.useLayoutEffect : _react.useEffect;
/**
* withFiled is used to inject components
* 1. Takes over the value and onChange of the component and synchronizes them to Form Foundation
* 2. Insert <Label>
* 3. Insert <ErrorMessage>
*/
function withField(Component, opts) {
let SemiField = (props, ref) => {
let {
// condition,
field,
label,
labelPosition,
labelWidth,
labelAlign,
labelCol,
wrapperCol,
noLabel,
noErrorMessage,
isInInputGroup,
initValue,
validate,
validateStatus,
trigger,
allowEmptyString,
allowEmpty,
emptyValue,
rules,
required,
keepState,
transform,
name,
fieldClassName,
fieldStyle,
convert,
stopValidateWithError,
helpText,
extraText,
extraTextPosition,
pure,
id,
rest
} = (0, _utils.mergeProps)(props);
let {
options,
shouldInject
} = (0, _utils.mergeOptions)(opts, props);
(0, _warning.default)(typeof field === 'undefined' && options.shouldInject, "[Semi Form]: 'field' is required, please check your props of Field Component");
// 无需注入的直接返回,eg:Group内的checkbox、radio
// Return without injection, eg: <Checkbox> / <Radio> inside CheckboxGroup/RadioGroup
if (!shouldInject) {
return /*#__PURE__*/_react.default.createElement(Component, Object.assign({}, rest, {
ref: ref
}));
}
// grab formState from context
const formState = (0, _index.useFormState)();
// grab formUpdater (the api for field to read/modify FormState) from context
const updater = (0, _index.useFormUpdater)();
if (!updater.getFormProps) {
(0, _warning.default)(true, '[Semi Form]: Field Component must be use inside the Form, please check your dom declaration');
return null;
}
let formProps = updater.getFormProps(['labelPosition', 'labelWidth', 'labelAlign', 'labelCol', 'wrapperCol', 'disabled', 'showValidateIcon', 'extraTextPosition', 'stopValidateWithError', 'trigger']);
let mergeLabelPos = labelPosition || formProps.labelPosition;
let mergeLabelWidth = labelWidth || formProps.labelWidth;
let mergeLabelAlign = labelAlign || formProps.labelAlign;
let mergeLabelCol = labelCol || formProps.labelCol;
let mergeWrapperCol = wrapperCol || formProps.wrapperCol;
let mergeExtraPos = extraTextPosition || formProps.extraTextPosition || 'bottom';
let mergeStopValidateWithError = (0, _utils.transformDefaultBooleanAPI)(stopValidateWithError, formProps.stopValidateWithError, false);
let mergeTrigger = (0, _utils.transformTrigger)(trigger, formProps.trigger);
// To prevent user forgetting to pass the field, use undefined as the key, and updater.getValue will get the wrong value.
let initValueInFormOpts = typeof field !== 'undefined' ? updater.getValue(field) : undefined; // Get the init value of form from formP rops.init Values Get the initial value set in the initValues of Form
let initVal = typeof initValue !== 'undefined' ? initValue : initValueInFormOpts;
// use arrayFieldState to fix issue 615
let arrayFieldState;
try {
arrayFieldState = (0, _index.useArrayFieldState)();
if (arrayFieldState) {
initVal = arrayFieldState.shouldUseInitValue && typeof initValue !== 'undefined' ? initValue : initValueInFormOpts;
}
} catch (err) {}
// FIXME typeof initVal
const [value, setValue, getVal] = (0, _index.useStateWithGetter)(typeof initVal !== undefined ? initVal : null);
const validateOnMount = mergeTrigger.includes('mount');
allowEmpty = allowEmpty || updater.getFormProps().allowEmpty;
// Error information: Array, String, undefined
const [error, setError, getError] = (0, _index.useStateWithGetter)();
const [touched, setTouched] = (0, _react.useState)();
const [cursor, setCursor, getCursor] = (0, _index.useStateWithGetter)(0);
const [status, setStatus] = (0, _react.useState)(validateStatus); // use props.validateStatus to init
const isUnmounted = (0, _react.useRef)(false);
const rulesRef = (0, _react.useRef)(rules);
const validateRef = (0, _react.useRef)(validate);
const validatePromise = (0, _react.useRef)(null);
// notNotify is true means that the onChange of the Form does not need to be triggered
// notUpdate is true means that this operation does not need to trigger the forceUpdate
const updateTouched = (isTouched, callOpts) => {
setTouched(isTouched);
updater.updateStateTouched(field, isTouched, callOpts);
};
const updateError = (errors, callOpts) => {
if (isUnmounted.current) {
return;
}
if (errors === getError()) {
// When the inspection result is unchanged, no need to update, saving a forceUpdate overhead
// When errors is an array, deepEqual is not used, and it is always treated as a need to update
// 检验结果不变时,无需更新,节省一次forceUpdate开销
// errors为数组时,不做deepEqual,始终当做需要更新处理
return;
}
setError(errors);
updater.updateStateError(field, errors, callOpts);
if (!(0, _utils.isValid)(errors)) {
setStatus('error');
} else {
setStatus('success');
}
};
const updateValue = (val, callOpts) => {
setValue(val);
let newOpts = Object.assign(Object.assign({}, callOpts), {
allowEmpty
});
updater.updateStateValue(field, val, newOpts);
};
const reset = () => {
let callOpts = {
notNotify: true,
notUpdate: true
};
// reset is called by the FormFoundaion uniformly. The field level does not need to trigger notify and update.
updateValue(initVal !== null ? initVal : undefined, callOpts);
updateError(undefined, callOpts);
updateTouched(undefined, callOpts);
setStatus('default');
};
// Execute the validation rules specified by rules
const _validateInternal = (val, callOpts) => {
let latestRules = rulesRef.current || [];
const validator = (0, _utils.generateValidatesFromRules)(field, latestRules);
const model = {
[field]: val
};
const rootPromise = new Promise((resolve, reject) => {
validator.validate(model, {
first: mergeStopValidateWithError
}, (errors, fields) => {}).then(res => {
if (isUnmounted.current || validatePromise.current !== rootPromise) {
console.warn(`[Semi Form]: When FieldComponent (${field}) has an unfinished validation process, you repeatedly trigger a new validation, the old validation will be abandoned, and will neither resolve nor reject. Usually this is an unreasonable practice. Please check your code.`);
return;
}
// validation passed
setStatus('success');
updateError(undefined, callOpts);
resolve({});
}).catch(err => {
if (isUnmounted.current || validatePromise.current !== rootPromise) {
console.warn(`[Semi Form]: When FieldComponent (${field}) has an unfinished validation process, you repeatedly trigger a new validation, the old validation will be abandoned, and will neither resolve nor reject. Usually this is an unreasonable practice. Please check your code.`);
return;
}
let {
errors,
fields
} = err;
if (errors && fields) {
let messages = errors.map(e => e.message);
if (messages.length === 1) {
messages = messages[0];
}
updateError(messages, callOpts);
if (!(0, _utils.isValid)(messages)) {
setStatus('error');
resolve(errors);
}
} else {
// Some grammatical errors in rules
setStatus('error');
updateError(err.message, callOpts);
resolve(err.message);
throw err;
}
});
});
validatePromise.current = rootPromise;
return rootPromise;
};
// execute custom validate function
const _validate = (val, values, callOpts) => {
const rootPromise = new Promise(resolve => {
let maybePromisedErrors;
// let errorThrowSync;
try {
maybePromisedErrors = validateRef.current(val, values);
} catch (err) {
// error throw by syncValidate
maybePromisedErrors = err;
}
if (maybePromisedErrors === undefined) {
resolve({});
updateError(undefined, callOpts);
} else if ((0, _isPromise.default)(maybePromisedErrors)) {
maybePromisedErrors.then(result => {
// If the async validate is outdated (a newer validate occurs), the result should be discarded
if (isUnmounted.current || validatePromise.current !== rootPromise) {
console.warn(`[Semi Form]: When Field: (${field}) has an unfinished validation process, you repeatedly trigger a new validation, the old validation will be abandoned, and will neither resolve nor reject. Usually this is an unreasonable practice. Please check your code.`);
return;
}
if ((0, _utils.isValid)(result)) {
// validate success,no need to do anything with result
updateError(undefined, callOpts);
resolve(null);
} else {
// validate failed
updateError(result, callOpts);
resolve(result);
}
});
} else {
if ((0, _utils.isValid)(maybePromisedErrors)) {
updateError(undefined, callOpts);
resolve(null);
} else {
updateError(maybePromisedErrors, callOpts);
resolve(maybePromisedErrors);
}
}
});
validatePromise.current = rootPromise;
return rootPromise;
};
const fieldValidate = (val, callOpts) => {
let finalVal = val;
let latestRules = rulesRef.current;
if (transform) {
finalVal = transform(val);
}
if (validateRef.current) {
return _validate(finalVal, updater.getValue(), callOpts);
} else if (latestRules) {
return _validateInternal(finalVal, callOpts);
}
return null;
};
/**
* parse / format
* validate when trigger
*
*/
const handleChange = function (newValue, e) {
let fnKey = options.onKeyChangeFnName;
if (fnKey in props && typeof props[options.onKeyChangeFnName] === 'function') {
for (var _len = arguments.length, other = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
other[_key - 2] = arguments[_key];
}
props[options.onKeyChangeFnName](newValue, e, ...other);
}
// support various type component
let val;
if (!options.valuePath) {
val = newValue;
} else {
val = ObjectUtil.get(newValue, options.valuePath);
}
// User can use convert function to updateValue before Component UI render
if (typeof convert === 'function') {
val = convert(val);
}
// TODO: allowEmptyString split into allowEmpty, emptyValue
// Added abandonment warning
// if (process.env.NODE_ENV !== 'production') {
// warning(allowEmptyString, `'allowEmptyString' will be de deprecated in next version, please replace with 'allowEmpty' & 'emptyValue'
// `)
// }
// set value to undefined if it's an empty string
// allowEmptyString={true} is equivalent to allowEmpty = {true} emptyValue = "
if (allowEmptyString || allowEmpty) {
if (val === '') {
// do nothing
}
} else {
if (val === emptyValue) {
val = undefined;
}
}
// maintain compoent cursor if needed
try {
if (e && e.target && e.target.selectionStart) {
setCursor(e.target.selectionStart);
}
} catch (err) {}
updateTouched(true, {
notNotify: true,
notUpdate: true
});
updateValue(val);
// only validate when trigger includes change
if (mergeTrigger.includes('change')) {
fieldValidate(val);
}
};
const handleBlur = function () {
if (props.onBlur) {
props.onBlur(...arguments);
}
if (!touched) {
updateTouched(true);
}
if (mergeTrigger.includes('blur')) {
let val = getVal();
fieldValidate(val);
}
};
/** Field level maintains a separate layer of data, which is convenient for Form to control Field to update the UI */
// The field level maintains a separate layer of data, which is convenient for the Form to control the Field for UI updates.
const fieldApi = {
setValue: updateValue,
setTouched: updateTouched,
setError: updateError,
reset,
validate: fieldValidate
};
const fieldState = {
value,
error,
touched,
status
};
// avoid hooks capture value, fixed issue 346
useIsomorphicEffect(() => {
rulesRef.current = rules;
validateRef.current = validate;
}, [rules, validate]);
useIsomorphicEffect(() => {
isUnmounted.current = false;
// exec validate once when trigger include 'mount'
if (validateOnMount) {
fieldValidate(value);
}
return () => {
isUnmounted.current = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// register when mounted,unregister when unmounted
// register again when field change
useIsomorphicEffect(() => {
// register
if (typeof field === 'undefined') {
return () => {};
}
// log('register: ' + field);
// field value may change after field component mounted, we use ref value here to get changed value
const refValue = getVal();
updater.register(field, {
value: refValue,
error,
touched,
status
}, {
field,
fieldApi,
keepState,
allowEmpty: allowEmpty || allowEmptyString
});
// return unRegister cb
return () => {
updater.unRegister(field);
// log('unRegister: ' + field);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [field]);
// id attribute to improve a11y
const a11yId = id ? id : field;
const labelId = `${a11yId}-label`;
const helpTextId = `${a11yId}-helpText`;
const extraTextId = `${a11yId}-extraText`;
const errorMessageId = `${a11yId}-errormessage`;
const FieldComponent = () => {
// prefer to use validateStatus which pass by user throught props
let blockStatus = validateStatus ? validateStatus : status;
const extraCls = (0, _classnames.default)(`${prefix}-field-extra`, {
[`${prefix}-field-extra-string`]: typeof extraText === 'string',
[`${prefix}-field-extra-middle`]: mergeExtraPos === 'middle',
[`${prefix}-field-extra-bottom`]: mergeExtraPos === 'bottom'
});
const extraContent = extraText ? /*#__PURE__*/_react.default.createElement("div", {
className: extraCls,
id: extraTextId,
"x-semi-prop": "extraText"
}, extraText) : null;
let newProps = Object.assign(Object.assign({
id: a11yId,
disabled: formProps.disabled
}, rest), {
ref,
onBlur: handleBlur,
[options.onKeyChangeFnName]: handleChange,
[options.valueKey]: value,
validateStatus: blockStatus,
'aria-required': required,
'aria-labelledby': labelId
});
if (name) {
newProps['name'] = name;
}
if (helpText) {
newProps['aria-describedby'] = extraText ? `${helpTextId} ${extraTextId}` : helpTextId;
}
if (extraText) {
newProps['aria-describedby'] = helpText ? `${helpTextId} ${extraTextId}` : extraTextId;
}
if (status === 'error') {
newProps['aria-errormessage'] = errorMessageId;
newProps['aria-invalid'] = true;
}
const fieldCls = (0, _classnames.default)({
[`${prefix}-field`]: true,
[`${prefix}-field-${name}`]: Boolean(name),
[fieldClassName]: Boolean(fieldClassName)
});
const fieldMaincls = (0, _classnames.default)({
[`${prefix}-field-main`]: true
});
if (mergeLabelPos === 'inset' && !noLabel) {
newProps.insetLabel = label || field;
newProps.insetLabelId = labelId;
if (typeof label === 'object' && !(0, _reactUtils.isElement)(label)) {
newProps.insetLabel = label.text;
newProps.insetLabelId = labelId;
}
}
const com = /*#__PURE__*/_react.default.createElement(Component, Object.assign({}, newProps));
// when use in InputGroup, no need to insert <Label>、<ErrorMessage> inside Field, just add it at Group
if (isInInputGroup) {
return com;
}
if (pure) {
let pureCls = (0, _classnames.default)(rest.className, {
[`${prefix}-field-pure`]: true,
[`${prefix}-field-${name}`]: Boolean(name),
[fieldClassName]: Boolean(fieldClassName)
});
newProps.className = pureCls;
return /*#__PURE__*/_react.default.createElement(Component, Object.assign({}, newProps));
}
let withCol = mergeLabelCol && mergeWrapperCol;
const labelColCls = mergeLabelAlign ? `${prefix}-col-${mergeLabelAlign}` : '';
// get label
let labelContent = null;
if (!noLabel && mergeLabelPos !== 'inset') {
let needSpread = typeof label === 'object' && !(0, _reactUtils.isElement)(label) ? label : {};
labelContent = /*#__PURE__*/_react.default.createElement(_label.default, Object.assign({
text: label || field,
id: labelId,
required: required,
name: a11yId || name || field,
width: mergeLabelWidth,
align: mergeLabelAlign
}, needSpread));
}
const fieldMainContent = /*#__PURE__*/_react.default.createElement("div", {
className: fieldMaincls
}, mergeExtraPos === 'middle' ? extraContent : null, com, !noErrorMessage ? (/*#__PURE__*/_react.default.createElement(_errorMessage.default, {
error: error,
validateStatus: blockStatus,
helpText: helpText,
helpTextId: helpTextId,
errorMessageId: errorMessageId,
showValidateIcon: formProps.showValidateIcon
})) : null, mergeExtraPos === 'bottom' ? extraContent : null);
const withColContent = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, mergeLabelPos === 'top' ? (/*#__PURE__*/_react.default.createElement("div", {
style: {
overflow: 'hidden'
}
}, /*#__PURE__*/_react.default.createElement(_grid.Col, Object.assign({}, mergeLabelCol, {
className: labelColCls
}), labelContent))) : (/*#__PURE__*/_react.default.createElement(_grid.Col, Object.assign({}, mergeLabelCol, {
className: labelColCls
}), labelContent)), /*#__PURE__*/_react.default.createElement(_grid.Col, Object.assign({}, mergeWrapperCol), fieldMainContent));
return /*#__PURE__*/_react.default.createElement("div", {
className: fieldCls,
style: fieldStyle,
"x-label-pos": mergeLabelPos,
"x-field-id": field,
"x-extra-pos": mergeExtraPos
}, withCol ? withColContent : (/*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, labelContent, fieldMainContent)));
};
// !important optimization
const shouldUpdate = [...Object.values(fieldState), ...Object.values(props), field, mergeLabelPos, mergeLabelAlign, formProps.disabled];
if (options.shouldMemo) {
// eslint-disable-next-line react-hooks/exhaustive-deps
return (0, _react.useMemo)(FieldComponent, [...shouldUpdate]);
} else {
// Some Custom Component with inner state shouldn't be memo, otherwise the component will not updated when the internal state is updated
return FieldComponent();
}
};
SemiField = /*#__PURE__*/(0, _react.forwardRef)(SemiField);
SemiField.displayName = (0, _utils.getDisplayName)(Component);
return SemiField;
}
// eslint-disable-next-line
var _default = exports.default = withField;
;