UNPKG

ohayolibs

Version:

Ohayo is a set of essential modules for ohayojp.

387 lines (339 loc) 11.8 kB
import { OhayoSFConfig } from '@ohayo/util'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; import { SF_SEQ } from '../const'; import { ErrorData } from '../errors'; import { SFFormValueChange, SFUpdateValueAndValidity, SFValue } from '../interface'; import { SFSchema, SFSchemaType } from '../schema'; import { SFUISchema, SFUISchemaItem, SFUISchemaItemRun } from '../schema/ui'; import { isBlank } from '../utils'; import { SchemaValidatorFactory } from '../validator.factory'; import { Widget } from '../widget'; export abstract class FormProperty { private _errors: ErrorData[] | null = null; private _valueChanges = new BehaviorSubject<SFFormValueChange>({ path: null, pathValue: null, value: null }); private _errorsChanges = new BehaviorSubject<ErrorData[] | null>(null); private _visible = true; private _visibilityChanges = new BehaviorSubject<boolean>(true); private _root: PropertyGroup; private _parent: PropertyGroup | null; _objErrors: { [key: string]: ErrorData[] } = {}; schemaValidator: (value: SFValue) => ErrorData[]; schema: SFSchema; ui: SFUISchema | SFUISchemaItemRun; formData: {}; _value: SFValue = null; widget: Widget<FormProperty, SFUISchemaItem>; path: string; constructor( schemaValidatorFactory: SchemaValidatorFactory, schema: SFSchema, ui: SFUISchema | SFUISchemaItem, formData: {}, parent: PropertyGroup | null, path: string, private _options: OhayoSFConfig, ) { this.schema = schema; this.ui = ui; this.schemaValidator = schemaValidatorFactory.createValidatorFn(schema, { ingoreKeywords: this.ui.ingoreKeywords as string[], debug: (ui as SFUISchemaItem)!.debug!, }); this.formData = formData || schema.default; this._parent = parent; if (parent) { this._root = parent.root; } else { this._root = this as any; } this.path = path; } get valueChanges(): BehaviorSubject<SFFormValueChange> { return this._valueChanges; } get errorsChanges(): BehaviorSubject<ErrorData[] | null> { return this._errorsChanges; } get type(): SFSchemaType { return this.schema.type!; } get parent(): PropertyGroup | null { return this._parent; } get root(): PropertyGroup { return this._root; } get value(): SFValue { return this._value; } get errors(): ErrorData[] | null { return this._errors; } get visible(): boolean { return this._visible; } get valid(): boolean { return this._errors === null || this._errors.length === 0; } get options(): OhayoSFConfig { return this._options; } /** * 设置值 * * @param onlySelf `true` 只对当前字段更新值和校验;`false` 包含上级字段 */ abstract setValue(value: SFValue, onlySelf: boolean): void; /** * 重置值,默认值为 `schema.default` * * @param onlySelf `true` 只对当前字段更新值和校验;`false` 包含上级字段 */ abstract resetValue(value: SFValue, onlySelf: boolean): void; /** * @internal */ abstract _hasValue(): boolean; /** * @internal */ abstract _updateValue(): void; /** * 更新值且校验数据 */ updateValueAndValidity(options?: SFUpdateValueAndValidity): void { options = { onlySelf: false, emitValidator: true, emitValueEvent: true, updatePath: '', updateValue: null, ...options }; this._updateValue(); if (options.emitValueEvent) { options.updatePath = options.updatePath || this.path; options.updateValue = options.updateValue || this.value; this.valueChanges.next({ value: this.value, path: options.updatePath, pathValue: options.updateValue }); } // `emitValidator` 每一次数据变更已经包含完整错误链路,后续父节点数据变更无须再触发校验 if (options.emitValidator && this.ui.liveValidate === true) { this._runValidation(); } if (this.parent && !options.onlySelf) { this.parent.updateValueAndValidity({ ...options, emitValidator: false }); } } /** 根据路径搜索表单属性 */ searchProperty(path: string): FormProperty | null { let prop: FormProperty = this; let base: PropertyGroup | null = null; let result = null; if (path[0] === SF_SEQ) { base = this.findRoot(); result = base.getProperty(path.substr(1)); } else { while (result === null && prop.parent !== null) { prop = base = prop.parent; result = base.getProperty(path); } } return result!; } /** 查找根表单属性 */ findRoot(): PropertyGroup { let property: FormProperty = this; while (property.parent !== null) { property = property.parent; } return property as PropertyGroup; } // #region process errors private isEmptyData(value: {}): boolean { if (isBlank(value)) return true; switch (this.type) { case 'string': return ('' + value).length === 0; } return false; } /** * @internal */ _runValidation(): void { let errors: ErrorData[]; // The definition of some rules: // 1. Should not ajv validator when is empty data and required fields // 2. Should not ajv validator when is empty data const isEmpty = this.isEmptyData(this._value); if (isEmpty && this.ui._required) { errors = [{ keyword: 'required' }]; } else if (isEmpty) { errors = []; } else { errors = this.schemaValidator(this._value) || []; } const customValidator = (this.ui as SFUISchemaItemRun).validator; if (typeof customValidator === 'function') { const customErrors = customValidator(this.value, this, this.findRoot()); if (customErrors instanceof Observable) { customErrors.subscribe(res => { this.setCustomErrors(errors, res); this.widget.detectChanges(); }); return; } this.setCustomErrors(errors, customErrors); return; } this._errors = errors; this.setErrors(this._errors); } private setCustomErrors(errors: ErrorData[], list: ErrorData[]): void { // fix error format const hasCustomError = list != null && list.length > 0; if (hasCustomError) { list.forEach(err => { if (!err.message) { throw new Error(`The custom validator must contain a 'message' attribute to viewed error text`); } err._custom = true; }); } this._errors = this.mergeErrors(errors, list); this.setErrors(this._errors); } private mergeErrors(errors: ErrorData[], newErrors: ErrorData | ErrorData[]): ErrorData[] { if (newErrors) { if (Array.isArray(newErrors)) { errors = errors.concat(...newErrors); } else { errors.push(newErrors); } } return errors; } protected setErrors(errors: ErrorData[], emitFormat: boolean = true): void { if (emitFormat && errors && !this.ui.onlyVisual) { const l = (this.widget && this.widget.l.error) || {}; errors = errors.map((err: ErrorData) => { let message = err._custom === true && err.message ? err.message : (this.ui.errors || {})[err.keyword] || this._options.errors![err.keyword] || l[err.keyword] || ``; if (message && typeof message === 'function') { message = message(err) as string; } if (message) { if (~(message as string).indexOf('{')) { message = (message as string).replace(/{([\.a-z0-9]+)}/g, (_v: string, key: string) => err.params![key] || ''); } err.message = message as string; } return err; }); } this._errors = errors; this._errorsChanges.next(errors); // Should send errors to parent field if (this._parent) { this._parent.setParentAndPlatErrors(errors, this.path); } } setParentAndPlatErrors(errors: ErrorData[], path: string): void { this._objErrors[path] = errors; const platErrors: ErrorData[] = []; Object.keys(this._objErrors).forEach(p => { const property = this.searchProperty(p); if (property && !property.visible) return; platErrors.push(...this._objErrors[p]); }); this.setErrors(platErrors, false); } // #endregion // #region condition private setVisible(visible: boolean): void { this._visible = visible; this._visibilityChanges.next(visible); // 部分数据源来自 reset if (this.root.widget?.sfComp?._inited === true) { this.resetValue(this.value, true); } } // A field is visible if AT LEAST ONE of the properties it depends on is visible AND has a value in the list _bindVisibility(): void { const visibleIf = (this.ui as SFUISchemaItem).visibleIf; if (typeof visibleIf === 'object' && Object.keys(visibleIf).length === 0) { this.setVisible(false); } else if (visibleIf !== undefined) { const propertiesBinding: Array<Observable<boolean>> = []; for (const dependencyPath in visibleIf) { if (visibleIf.hasOwnProperty(dependencyPath)) { const property = this.searchProperty(dependencyPath); if (property) { const valueCheck = property.valueChanges.pipe( map(res => { const vi = visibleIf[dependencyPath]; if (typeof vi === 'function') { return vi(res.value); } if (vi.indexOf('$ANY$') !== -1) { return res.value.length > 0; } else { return vi.indexOf(res.value) !== -1; } }), ); const visibilityCheck = property._visibilityChanges; const and = combineLatest([valueCheck, visibilityCheck]).pipe(map(results => results[0] && results[1])); propertiesBinding.push(and); } else { console.warn(`Can't find property ${dependencyPath} for visibility check of ${this.path}`); } } } combineLatest(propertiesBinding) .pipe( map(values => values.indexOf(true) !== -1), distinctUntilChanged(), ) .subscribe(visible => this.setVisible(visible)); } } // #endregion } export abstract class PropertyGroup extends FormProperty { properties: { [key: string]: FormProperty } | FormProperty[] | null = null; getProperty(path: string): FormProperty | undefined { const subPathIdx = path.indexOf(SF_SEQ); const propertyId = subPathIdx !== -1 ? path.substr(0, subPathIdx) : path; let property = (this.properties as { [key: string]: FormProperty })[propertyId]; if (property !== null && subPathIdx !== -1 && property instanceof PropertyGroup) { const subPath = path.substr(subPathIdx + 1); property = (property as PropertyGroup).getProperty(subPath)!; } return property; } forEachChild(fn: (formProperty: FormProperty, str: string) => void): void { for (const propertyId in this.properties) { if (this.properties.hasOwnProperty(propertyId)) { const property = (this.properties as { [key: string]: FormProperty })[propertyId]; fn(property, propertyId); } } } forEachChildRecursive(fn: (formProperty: FormProperty) => void): void { this.forEachChild(child => { fn(child); if (child instanceof PropertyGroup) { (child as PropertyGroup).forEachChildRecursive(fn); } }); } _bindVisibility(): void { super._bindVisibility(); this._bindVisibilityRecursive(); } private _bindVisibilityRecursive(): void { this.forEachChildRecursive(property => { property._bindVisibility(); }); } isRoot(): boolean { return this === this.root; } }