UNPKG

@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
"use strict"; 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;