import React from 'react'; import {IFormStore, IFormItemStore} from '../../store/form'; import debouce from 'lodash/debounce'; import {RendererProps, Renderer} from '../../factory'; import {ComboStore, IComboStore, IUniqueGroup} from '../../store/combo'; import { anyChanged, promisify, isObject, guid, isEmpty, autobind, getVariable } from '../../utils/helper'; import {IIRendererStore, IRendererStore} from '../../store'; import {ScopedContext, IScopedContext} from '../../Scoped'; import {reaction} from 'mobx'; import {FormItemStore} from '../../store/formItem'; import {isAlive} from 'mobx-state-tree'; import {observer} from 'mobx-react'; import hoistNonReactStatic from 'hoist-non-react-statics'; import {withRootStore} from '../../WithRootStore'; import {FormBaseControl, FormItemWrap} from './Item'; export interface ControlOutterProps extends RendererProps { formStore?: IFormStore; name?: string; value?: any; id?: string; type?: string; required?: boolean; validations: string | {[propsName: string]: any}; validationErrors: {[propsName: string]: any}; validateOnChange: boolean; multiple?: boolean; delimiter?: string; joinValues?: boolean; extractValue?: boolean; valueField?: string; labelField?: string; unique?: boolean; selectFirst?: boolean; autoFill?: any; clearValueOnHidden?: boolean; submitOnChange?: boolean; validate?: (value: any, values: any, name: string) => any; formItem?: IFormItemStore; addHook?: (fn: () => any, type?: 'validate' | 'init' | 'flush') => void; removeHook?: (fn: () => any, type?: 'validate' | 'init' | 'flush') => void; $schema: { pipeIn?: (value: any, data: any) => any; pipeOut?: (value: any, originValue: any, data: any) => any; [propName: string]: any; }; store?: IIRendererStore; onChange?: ( value: any, name: string, submit?: boolean, changePristine?: boolean ) => void; } export interface ControlProps { onBulkChange?: (values: Object) => void; onChange?: (value: any, name: string, submit: boolean) => void; store: IIRendererStore; } export function wrapControl< T extends React.ComponentType & ControlProps> >(ComposedComponent: T) { type OuterProps = JSX.LibraryManagedAttributes< T, Omit, keyof ControlProps> > & ControlOutterProps; const result = hoistNonReactStatic( withRootStore( observer( class extends React.Component { public model: IFormItemStore | undefined; control?: any; value?: any = undefined; hook?: () => any; hook2?: () => any; hook3?: () => any; reaction?: () => void; static contextType = ScopedContext; static defaultProps = {}; lazyEmitChange = debouce(this.emitChange.bind(this), 250, { trailing: true, leading: false }); constructor(props: OuterProps) { super(props); const { formStore: form, formItem, rootStore, store, onChange, canAccessSuperData, data, $schema: { name, id, type, required, validations, validationErrors, unique, value, multiple, delimiter, valueField, labelField, joinValues, extractValue, selectFirst, autoFill, clearValueOnHidden } } = this.props; this.getValue = this.getValue.bind(this); this.setValue = this.setValue.bind(this); this.handleChange = this.handleChange.bind(this); this.setPrinstineValue = this.setPrinstineValue.bind(this); this.controlRef = this.controlRef.bind(this); this.handleBlur = this.handleBlur.bind(this); if (!name) { return; } let propValue = this.props.value !== value ? this.props.value : undefined; const model = rootStore.addStore({ id: guid(), path: this.props.$path, storeType: FormItemStore.name, parentId: store!.id, name }) as IFormItemStore; this.model = model; // @issue 打算干掉这个 formItem?.addSubFormItem(model); model.config( { id, type, required, unique, value, rules: validations, messages: validationErrors, multiple, delimiter, valueField, labelField, joinValues, extractValue, selectFirst, autoFill, clearValueOnHidden }, onChange ); // issue 这个逻辑应该在 combo 里面自己实现。 if ( this.model.unique && form?.parentStore?.storeType === ComboStore.name ) { const combo = form.parentStore as IComboStore; combo.bindUniuqueItem(model); } // 同步 value model.changeTmpValue( propValue ?? store?.getValueByName(model.name, canAccessSuperData) ?? value ); // 如果没有初始值,通过 onChange 设置过去 if ( onChange && typeof propValue === 'undefined' && typeof store?.getValueByName(model.name, false) === 'undefined' ) { onChange(model.tmpValue, model.name, false, true); } } componentDidMount() { const { store, formStore: form, $schema: {name, validate}, addHook } = this.props; // 提交前先把之前的 lazyEmit 执行一下。 this.hook3 = () => { this.lazyEmitChange.flush(); }; addHook?.(this.hook3, 'flush'); const formItem = this.model as IFormItemStore; if (formItem && validate) { let finalValidate = promisify(validate.bind(formItem)); this.hook2 = function () { formItem.clearError('control:valdiate'); return finalValidate( this.props.data, this.getValue(), name ).then((ret: any) => { if ((typeof ret === 'string' || Array.isArray(ret)) && ret) { formItem.addError(ret, 'control:valdiate'); } }); }; addHook?.(this.hook2); } } componentDidUpdate(prevProps: OuterProps) { const props = this.props; const form = props.formStore; const model = this.model; if ( model && anyChanged( [ 'id', 'validations', 'validationErrors', 'value', 'required', 'unique', 'multiple', 'delimiter', 'valueField', 'labelField', 'joinValues', 'extractValue', 'selectFirst', 'autoFill', 'clearValueOnHidden' ], prevProps.$schema, props.$schema ) ) { model.config( { required: props.$schema.required, id: props.$schema.id, unique: props.$schema.unique, value: props.$schema.value, rules: props.$schema.validations, multiple: props.$schema.multiple, delimiter: props.$schema.delimiter, valueField: props.$schema.valueField, labelField: props.$schema.labelField, joinValues: props.$schema.joinValues, extractValue: props.$schema.extractValue, messages: props.$schema.validationErrors, selectFirst: props.$schema.selectFirst, autoFill: props.$schema.autoFill, clearValueOnHidden: props.$schema.clearValueOnHidden }, props.onChange ); } let modified = false; if (model && props.value !== prevProps.value) { if (props.value !== model.tmpValue) { model.changeTmpValue(props.value); modified = true; } } else if ( model && props.data !== prevProps.data && (!model.emitedValue || model.emitedValue === model.tmpValue) ) { model.changeEmitedValue(undefined); const value = getVariable( props.data, model.name, props.canAccessSuperData !== false ); const prevValue = getVariable( prevProps.data, model.name, props.canAccessSuperData !== false ); if (value !== prevValue && value !== model.tmpValue) { model.changeTmpValue(value); modified = true; } } } componentWillUnmount() { this.hook && this.props.removeHook?.(this.hook); this.hook2 && this.props.removeHook?.(this.hook2); this.hook3 && this.props.removeHook?.(this.hook3, 'flush'); // this.lazyEmitChange.flush(); if (this.model?.clearValueOnHidden) { this.props.onChange?.(undefined, this.model.name); } this.lazyEmitChange.cancel(); this.reaction?.(); this.disposeModel(); } disposeModel() { const {formStore: form, formItem} = this.props; if ( this.model && this.model.unique && form?.parentStore && form?.parentStore.storeType === ComboStore.name ) { const combo = form.parentStore as IComboStore; combo.unBindUniuqueItem(this.model); } this.model && formItem && isAlive(formItem) && formItem.removeSubFormItem(this.model); } controlRef(control: any) { const { addHook, removeHook, formStore: form, $schema: {name} } = this.props; // 因为 control 有可能被 n 层 hoc 包裹。 while (control && control.getWrappedInstance) { control = control.getWrappedInstance(); } if (control && control.validate && this.model) { const formItem = this.model as IFormItemStore; let validate = promisify(control.validate.bind(control)); this.hook = () => { formItem.clearError('component:valdiate'); return validate(this.props.data, this.getValue(), name).then( ret => { if ( (typeof ret === 'string' || Array.isArray(ret)) && ret ) { formItem.setError(ret, 'component:valdiate'); } } ); }; addHook?.(this.hook); } else if (!control && this.hook) { removeHook?.(this.hook); this.hook = undefined; } // 注册到 Scoped 上 const originRef = this.control; this.control = control; const scoped = this.context as IScopedContext; if (!this.control) { return; } if (control) { scoped.registerComponent(this.control); } else { scoped.unRegisterComponent(originRef); } } validate() { const {formStore: form, data} = this.props; if (this.model) { if ( this.model.unique && form?.parentStore && form.parentStore.storeType === ComboStore.name ) { const combo = form.parentStore as IComboStore; const group = combo.uniques.get( this.model.name ) as IUniqueGroup; group.items.forEach(item => item.validate(data)); } else { this.model.validate(data, this.hook); form ?.getItemsByName(this.model.name) .forEach(item => item !== this.model && item.validate(data)); } } } handleChange( value: any, submitOnChange: boolean = this.props.$schema.submitOnChange, changeImmediately: boolean = false ) { const { formStore: form, onChange, $schema: { type, pipeOut, changeImmediately: conrolChangeImmediately }, formInited, data } = this.props; if ( !this.model || // todo 以后想办法不要強耦合类型。 ~[ 'service', 'group', 'hbox', 'panel', 'grid', 'input-group' ].indexOf(type) ) { onChange && onChange.apply(null, arguments as any); return; } if (pipeOut) { const oldValue = this.model.value; value = pipeOut(value, oldValue, data); } this.model.changeTmpValue(value); if (changeImmediately || conrolChangeImmediately || !formInited) { this.emitChange(submitOnChange); } else { // this.props.onTmpValueChange?.(value, this.model.name); this.lazyEmitChange(submitOnChange); } } emitChange( submitOnChange: boolean = this.props.$schema.submitOnChange ) { const { formStore: form, onChange, $schema: {name, onChange: onFormItemChange}, data, canAccessSuperData, validateOnChange, formSubmited } = this.props; if (!this.model) { return; } const value = this.model.tmpValue; const oldValue = getVariable( data, this.model.name, canAccessSuperData !== false ); if (oldValue === value) { return; } this.model.changeEmitedValue(value); if ( onFormItemChange?.(value, oldValue, this.model, form) === false ) { return; } onChange?.(value, name!, submitOnChange === true); if ( validateOnChange === true || (validateOnChange !== false && (formSubmited || this.model.validated)) ) { this.validate(); } else if (validateOnChange === false) { this.model.reset(); } } handleBlur(e: any) { const { onBlur, $schema: {validateOnBlur} } = this.props; if (validateOnBlur && this.model) { this.validate(); } onBlur && onBlur(e); } setPrinstineValue(value: any) { if (!this.model) { return; } const { formStore: form, name, $schema: {pipeOut}, onChange, value: oldValue, data } = this.props; if (pipeOut) { value = pipeOut(value, oldValue, data); } onChange?.(value, name!, false, true); } getValue() { const {formStore: data, $schema: control} = this.props; let value: any = this.model ? this.model.tmpValue : control.value; if (control.pipeIn) { value = control.pipeIn(value, data); } return value; } // 兼容老版本用法,新版本直接用 onChange 就可以。 setValue(value: any, key?: string) { const { $schema: {name}, onBulkChange } = this.props; if (!key || key === name) { this.handleChange(value); } else { onBulkChange({ [key]: value }); } } render() { const { controlWidth, disabled, formMode, $schema: control, store, data } = this.props; const value = this.getValue(); const model = this.model; const injectedProps: any = { defaultSize: controlWidth, disabled: disabled || control.disabled, formItem: this.model, formMode: control.mode || formMode, ref: this.controlRef, data: store ? store.data : data, value, defaultValue: control.value, formItemValue: value, // 为了兼容老版本的自定义组件 onChange: this.handleChange, onBlur: this.handleBlur, setValue: this.setValue, getValue: this.getValue, prinstine: model ? model.prinstine : undefined, setPrinstineValue: this.setPrinstineValue }; return ( >)} {...injectedProps} /> ); } } ) as any ), ComposedComponent ); return result as typeof result & { ComposedComponent: T; }; }