UNPKG

zent

Version:

一套前端设计语言和基于React的实现

658 lines (586 loc) 18.4 kB
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import identity from '../../../utils/identity'; import { BasicModel } from './basic'; import { ValidateOption } from '../validate'; import { INormalizeBeforeSubmit } from './field'; import { ModelRef } from './ref'; import type { BasicBuilder } from '../builders/basic'; import { get, or, Some } from '../maybe'; import { IModel } from './base'; import isNil from '../../../utils/isNil'; import uniqueId from '../../../utils/uniqueId'; import { pairwise, skip } from 'rxjs/operators'; import { createUnexpectedModelError } from '../error'; import { warningSubscribeValid, warningSubscribeValue } from '../warnings'; import { FIELD_ARRAY_ID, isModelRef, isModel } from './is'; import type { FieldArrayBuilder } from '../builders'; import { createSentinelSubject } from './sentinel-subject'; class FieldArrayModel< Item, Child extends IModel<Item> = IModel<Item> > extends BasicModel<readonly Item[]> { /** * @internal */ [FIELD_ARRAY_ID]!: boolean; protected readonly _displayName = 'FieldArrayModel'; readonly children$: BehaviorSubject<Child[]>; owner: IModel<any> | null = null; /** * 当前 `FieldArrayModel` 对象的 builder 对象,仅在 `Model` 模式下可用。 */ readonly builder?: FieldArrayBuilder<BasicBuilder<Item, Child>>; private _valid$?: BehaviorSubject<boolean>; private _value$?: BehaviorSubject<readonly Item[]>; private readonly invalidModels: Set<BasicModel<Item>> = new Set(); private readonly mapModelToSubscriptions: Map<IModel<any>, Subscription[]> = new Map(); private readonly childFactory: (defaultValue: Item) => Child; /** * 用于表单提交前格式化 `FieldArray` 值的回调函数 */ normalizeBeforeSubmit: INormalizeBeforeSubmit<Item[], any> = identity; /** @internal */ constructor( childBuilder: BasicBuilder<Item, Child> | null, private readonly defaultValue: readonly Item[] ) { super(uniqueId('field-array-')); this.childFactory = childBuilder ? (defaultValue: Item) => { const child = childBuilder.build(Some(defaultValue)); return this._linkChild(child); } : (defaultValue: Item) => new ModelRef<Item, FieldArrayModel<Item, Child>, Child>( null, Some(defaultValue), this ) as unknown as Child; const children = this.defaultValue.map(this._buildChild); this.children$ = new BehaviorSubject(children); } get value() { if (this._value$) { return this._value$.value; } return this.getRawValue(); } get value$() { return this._getValue$(true); } get valid$() { return this._getValid$(true); } /** * @internal * * The same as value$, but without warning */ _getValue$(shouldWarn = false) { warningSubscribeValue(shouldWarn, this._displayName); if (!this._value$) { this._initValue$(); } return this._value$; } _getValid$(shouldWarn = false) { warningSubscribeValid(shouldWarn, this._displayName); if (!this._valid$) { this._initValid$(); } return this._valid$; } /** * 重置 `FieldArray` 为初始值,初始值通过 `initialize` 设置;如果初始值不存在就使用默认值 */ reset() { const children = or(this.initialValue, () => this.defaultValue).map( this._buildChild ); this.children$.next(children); } /** * 清除 `FieldArray` 的初始值,并将当前值设置为默认值 */ clear() { this.initialValue = undefined; const children = this.defaultValue.map(this._buildChild); this.children$.next(children); } /** * 清除 `FieldArray` 所有字段的错误信息 */ clearError() { this.error$.next(null); const children = this.children; const length = children.length; for (let i = 0; i < length; i++) { const element = children[i]; element.clearError(); } } /** * 获取 `FieldArray` 内的所有 model */ get children(): ReadonlyArray<Child> { return this.children$.getValue(); } /** * 获取指定下标的子 model * @param index child model index */ get(index: number): Child { return this.children[index]; } /** * 获取 `FieldArray` 内的原始值 */ getRawValue() { return this._getValue(model => model.getRawValue()); } /** * 获取 `FieldArray` 的用于表单提交的值,和原始值可能不一致 */ getSubmitValue() { const value = this._getValue(model => model.getSubmitValue()); return this.normalizeBeforeSubmit(value); } /** * 修改 `FieldArray` 的值 * @param value 要修改的值 */ patchValue(value: Item[]) { const children = this.children$.getValue(); for (let i = 0; i < value.length; i += 1) { if (i >= children.length) { break; } const item = value[i]; const model = children[i]; if (isModelRef(model)) { const m = model.getModel(); m && m.patchValue(item); } else if (isModel(model)) { model.patchValue(item); } } if (value.length <= children.length) { this.splice(value.length, children.length - value.length); return; } for (let i = children.length; i < value.length; i += 1) { const item = value[i]; this.push(item); } } /** * 初始化 `FieldArray` 的值,同时设置 `initialValue` * @param values 要设置为初始化值的值 */ initialize(values: Item[]) { this.initialValue = Some(values); const children = values.map(this._buildChild); this.children$.next(children); } /** * 添加一批元素到 `FieldArray` 的末尾 * @param models 待添加的 `Model` 对象 */ push(...models: Child[]): number; /** * 添加一批元素到 `FieldArray` 的末尾 * @param values 待添加的值 */ push(...values: Item[]): number; push(...items: Item[] | Child[]) { const nextChildren = this.children$.getValue().concat( (items.map as any)((item: Item | Child) => { return isModel(item) ? this._linkChild(item as Child) : this._buildChild(item as Item); }) ); this.children$.next(nextChildren); // Same as `Array.prototype.push` return nextChildren.length; } /** * 删除 `FieldArray` 最后的一个元素。 * @return `Model` 对象,而不是 `Model` 对象上的值。 */ pop() { const children = this.children$.getValue().slice(); const child = children.pop(); child && this._disposeChild(child); this.children$.next(children); return child; } /** * 删除 `FieldArray` 第一个元素 * @return `Model` 对象,而不是 `Model` 对象上的值。 */ shift() { const children = this.children$.getValue().slice(); const child = children.shift(); child && this._disposeChild(child); this.children$.next(children); return child; } /** * 在 `FieldArray` 开头添加值 * @param models 待添加的 `Model` 对象 */ unshift(...models: Child[]): number; /** * 在 `FieldArray` 开头添加值 * @param values 待添加的值 */ unshift(...values: Item[]): number; unshift(...items: Item[] | Child[]) { const nextChildren = (items.map as any)((item: Item | Child) => { return isModel(item) ? this._linkChild(item as Child) : this._buildChild(item as Item); }).concat(this.children$.getValue()); this.children$.next(nextChildren); return nextChildren.length; } /** * 在 `FieldArray` 的指定位置删除指定数量的元素,并添加指定的新元素 * @param start 开始删除的元素位置 * @param deleteCount 删除的元素个数 * @param models 待添加的 `Model` * @return `Model` 对象,而不是 `Model` 对象上的值 */ splice( start: number, deleteCount: number, ...models: readonly Child[] ): Child[]; /** * 在 `FieldArray` 的指定位置删除指定数量的元素,并添加指定的新元素 * @param start 开始删除的元素位置 * @param deleteCount 删除的元素个数 * @param values 待添加的元素值 * @return `Model` 对象,而不是 `Model` 对象上的值 */ splice( start: number, deleteCount: number, ...values: readonly Item[] ): Child[]; splice( start: number, deleteCount = 0, ...items: readonly Item[] | readonly Child[] ): Child[] { const children = this.children$.getValue().slice(); const insertedChildren = (items.map as any)((item: Item | Child) => { return isModel(item) ? this._linkChild(item as Child) : this._buildChild(item as Item); }); const removedChildren = children.splice( start, deleteCount, ...insertedChildren ); removedChildren.forEach(this._disposeChild); this.children$.next(children); return removedChildren; } /** * 仅保留 `predicate` 返回 `true` 的子 `Model`。用于按一定条件批量过滤,相比重复调用 `splice` 效率更高。 * 该方法直接操作当前的 `FieldArrayModel` 对象。 * @param predicate 每个子 `Model` 的 `predicate` 函数,返回 `true` 在结果中保留该 `Model` * @return 当前 `FieldArrayModel` 对象 */ filter( predicate: (item: Child, index: number, array: Child[]) => boolean ): this { const children = this.children$.getValue().filter((item, index, array) => { const keep = predicate(item, index, array); if (!keep) { this._disposeChild(item); } return keep; }); this.children$.next(children); return this; } /** * 对 `FieldArrayModel` 的子 `Model` 排序,该方法直接操作当前的 `FieldArrayModel` 对象。 * @param compareFn 比较函数,行为和 `Array.prototype.sort` 的比较函数一致 * @return 当前 `FieldArrayModel` 对象 */ sort(compareFn: (a: Child, b: Child) => number): this { const children = this.children$.getValue().slice().sort(compareFn); this.children$.next(children); return this; } /** * 执行 `FieldArray` 的校验 * @param option 校验的参数 */ validate(option = ValidateOption.Default): Promise<any> { if (option & ValidateOption.IncludeChildrenRecursively) { const childOption = option | ValidateOption.StopPropagation; return Promise.all( this.children$ .getValue() .map(it => it.validate(childOption)) .concat(this.triggerValidate(option)) ); } return this.triggerValidate(option); } /** * 是否 `FieldArray` 所有元素都没有修改过 */ pristine() { const children = this.children$.getValue(); for (let i = 0; i < children.length; i += 1) { const child = children[i]; if (child.dirty()) { return false; } } return true; } /** * 是否 `FieldArray` 中任意元素有过修改 * * `dirty === !pristine` */ dirty() { return !this.pristine(); } /** * 是否 `FieldArray` 任意元素被 touch 过 */ touched() { const children = this.children$.getValue(); for (let i = 0; i < children.length; i += 1) { const child = children[i]; if (child.touched()) { return true; } } return false; } dispose() { super.dispose(); this.children.forEach(child => { this._unsubscribeChild(child); child.dispose(); }); this.children$.next([]); // Close all subjects and setup sentinels to warn use after free errors this.children$.complete(); this._value$?.complete(); this._valid$?.complete(); this._valid$ = createSentinelSubject(this._displayName, false); this._value$ = createSentinelSubject(this._displayName, []); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion (this.children$ as BehaviorSubject<Child[]>) = createSentinelSubject( this._displayName, [] ); } private _linkChild(child: Child) { child.owner = this; return child; } private _initValue$() { const value$ = new BehaviorSubject<readonly Item[]>(this.getRawValue()); this._value$ = value$; /** Skip the first subscription to avoid setting initialValue repeatedly */ this.children$.pipe(skip(1)).subscribe(() => { value$.next(this.getRawValue()); }); for (const child of this.children) { this._subscribeChild(child); } /** Do it if there's no initialized observable */ if (!this._valid$) { this._initUnsubscribeChild(); } } private _initValid$() { const valid$ = new BehaviorSubject(isNil(this.error)); this._valid$ = valid$; const $ = this.error$.subscribe(maybeError => { const selfValid = isNil(maybeError); valid$.next(selfValid && !this.invalidModels.size); }); this.mapModelToSubscriptions.set(this, [$]); /** Skip the first subscription to avoid setting initialValue repeatedly */ this.children$.pipe(skip(1)).subscribe(() => { /** Emit valid$ when children removed */ valid$.next(isNil(this.error) && !this.invalidModels.size); }); for (const child of this.children) { this._subscribeChild(child); } /** Do it if there's no initialized observable */ if (!this._value$) { this._initUnsubscribeChild(); } } /** * Subscribe `children$` to unsubscribe the removed child */ private _initUnsubscribeChild() { this.children$.pipe(pairwise()).subscribe(([prev, current]) => { for (const child of prev) { if (!current.includes(child)) { this._unsubscribeChild(child); } } }); } /** * Base method for getting value from array model * @param getter map model to value */ private _getValue<V>(getter: (model: BasicModel<Item>) => V): V[] { return this.children$.getValue().map(child => { if (isModelRef<Item, this, BasicModel<Item>>(child)) { const model = child.getModel(); return isModel<Item>(model) ? getter(model) : (get(child.initialValue) as unknown as V); } else if (isModel<Item>(child)) { return getter(child); } throw createUnexpectedModelError(child); }); } /** * Handle different types of the child */ private _subscribeChild(child: Child) { const { _valid$, _value$, mapModelToSubscriptions } = this; if (_valid$ || _value$) { if (isModelRef<Item, FieldArrayModel<Item, Child>, Child>(child)) { /** Subscribe current model immediately */ const model = child.getModel(); if (isModel<Item>(model)) { this._subscribeChildModel(model); } /** Replace subscription while model updated */ const $ = child.model$.pipe(pairwise()).subscribe(([prev, current]) => { prev && this._unsubscribeChild(prev); if (isModel<Item>(current)) { this._subscribeChildModel(current); } }); mapModelToSubscriptions.set(child, [$]); } else if (isModel<Item>(child)) { this._subscribeChildModel(child); } } } /** * Subscribe `valid$` and `value$` of the child * @param model */ private _subscribeChildModel(model: BasicModel<Item>) { const { error$, _valid$, _value$, invalidModels } = this; if (_valid$) { this._subscribeObservable(model, model._getValid$(), valid => { if (valid) { invalidModels.delete(model); } else { invalidModels.add(model); } _valid$.next(!invalidModels.size && isNil(error$.value)); }); } if (_value$) { this._subscribeObservable( model, model._getValue$(), childValue => { const index = this.children.findIndex(it => { if ( isModelRef<Item, FieldArrayModel<Item, Child>, BasicModel<Item>>( it ) ) { return it.getModel() === model; } else if (isModel<Item>(it)) { return it === model; } else { throw createUnexpectedModelError(it); } }); const copy = [..._value$.value]; copy.splice(index, 1, childValue); _value$.next(copy); }, true /** New value will be inserted in the observer of `children$`, skip the first subscription when inserting a new child */ ); } } /** * Subscribe a specified observable of the model * @param model as the key for mapping to subscription * @param observable * @param observer * @param skipFirst skip the first subscription */ private _subscribeObservable<T>( model: BasicModel<Item>, observable: Observable<T>, observer: (value: T) => void, skipFirst?: boolean ) { const { mapModelToSubscriptions } = this; const $ = (skipFirst ? observable.pipe(skip(1)) : observable).subscribe( observer ); const subs = mapModelToSubscriptions.get(model); if (subs) { subs.push($); } else { mapModelToSubscriptions.set(model, [$]); } } /** * Unsubscribe `valid$` and `value$` of the model */ private _unsubscribeChild(child: Child) { let model: BasicModel<Item> | null = null; if (isModel<Item>(child)) { this.invalidModels.delete(child); model = child; } else if (isModelRef<Item, this, BasicModel<Item>>(child)) { model = child.getModel(); } this._unsubscribeModel(child); if (model) { this._unsubscribeModel(model); } } private _disposeChild = (child: Child) => { this._unsubscribeChild(child); child.owner = null; }; private _buildChild = (child: Item) => { const model = this.childFactory(child); this._subscribeChild(model); return model; }; private _unsubscribeModel(model: IModel<Item>) { const subs = this.mapModelToSubscriptions.get(model); subs?.forEach(sub => sub.unsubscribe()); this.mapModelToSubscriptions.delete(model); if (isModel<Item>(model)) { this.invalidModels.delete(model); } } } FieldArrayModel.prototype[FIELD_ARRAY_ID] = true; export { FieldArrayModel };