zent
Version:
一套前端设计语言和基于React的实现
468 lines (409 loc) • 12.5 kB
text/typescript
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { hasOwnProperty } from '../../../utils/hasOwn';
import identity from '../../../utils/identity';
import isNil from '../../../utils/isNil';
import isPlainObject from '../../../utils/isPlainObject';
import omit from '../../../utils/omit';
import uniqueId from '../../../utils/uniqueId';
import type { FieldSetBuilder } from '../builders/set';
import { Maybe, None, Some } from '../maybe';
import type {
UnknownFieldSetBuilderChildren,
UnknownFieldSetModelChildren,
} from '../utils';
import { IMaybeError, ValidateOption } from '../validate';
import { warningSubscribeValid, warningSubscribeValue } from '../warnings';
import { IModel } from './base';
import { BasicModel } from './basic';
import { INormalizeBeforeSubmit } from './field';
import { isModel, SET_ID } from './is';
import { createSentinelSubject } from './sentinel-subject';
type $FieldSetValue<Children extends UnknownFieldSetModelChildren> = {
[Key in keyof Children]: Children[Key] extends IModel<infer V> ? V : never;
};
class FieldSetModel<
Children extends UnknownFieldSetModelChildren = UnknownFieldSetModelChildren
> extends BasicModel<$FieldSetValue<Children>> {
/**
* @internal
*/
[SET_ID]!: boolean;
protected readonly _displayName: string = 'FieldSetModel';
/**
* 上层调用 `patchValue` 的时候,子组件可能是没被挂载的状态,这时候需要用 `patchedValue` 存一下值,子组件挂载的时候从这里读
* @internal
*/
patchedValue: Partial<$FieldSetValue<Children>> | null = null;
readonly childRegister$ = new Subject<string>();
readonly childRemove$ = new Subject<string>();
readonly children: Children = {} as Children;
owner: IModel<any> | null = null;
/**
* 当前 `FieldSetModel` 对象的 builder 对象,仅在 `Model` 模式下可用。
*/
readonly builder?: FieldSetBuilder<UnknownFieldSetBuilderChildren>;
private _valid$?: BehaviorSubject<boolean>;
private _value$?: BehaviorSubject<$FieldSetValue<Children>>;
private readonly invalidModels: Set<BasicModel<unknown>> = new Set();
private readonly mapModelToSubscriptions: Map<
BasicModel<unknown>,
Subscription[]
> = new Map();
/**
* 用于表单提交前格式化 `Field` 值的回调函数
*/
normalizeBeforeSubmit: INormalizeBeforeSubmit<$FieldSetValue<Children>, any> =
identity;
/** @internal */
constructor(children: Children, id = uniqueId('field-set-')) {
super(id);
const keys = Object.keys(children);
const keysLength = keys.length;
for (let index = 0; index < keysLength; index++) {
const name = keys[index];
const child = children[name];
this.registerChild(name, child);
}
this.children = 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);
}
_getValid$(shouldWarn = false) {
warningSubscribeValid(shouldWarn, this._displayName);
if (!this._valid$) {
this._initValid$();
}
return this._valid$;
}
_getValue$(shouldWarn = false) {
warningSubscribeValue(shouldWarn, this._displayName);
if (!this._value$) {
this._initValue$();
}
return this._value$;
}
/**
* 初始化 `FieldSet` 的值,并设置 `initialValue`
* @param values 待初始化的值
*/
initialize(values: $FieldSetValue<Children>) {
if (!isPlainObject(values)) {
return;
}
this.initialValue = Some(values);
const keys = Object.keys(values);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const child = this.children[key] as BasicModel<unknown>;
if (isModel(child)) {
child.initialize(values[key]);
}
}
}
/**
* @internal
*/
getPatchedValue<T>(name: string): Maybe<T> {
if (this.patchedValue && name in this.patchedValue) {
return Some<T>(this.patchedValue[name] as T);
}
return None();
}
/**
* 获取 `FieldSet` 的值
*/
getRawValue(): $FieldSetValue<Children> {
const value: any = {};
const childrenKeys = Object.keys(this.children);
for (let i = 0; i < childrenKeys.length; i++) {
const key = childrenKeys[i];
const model = this.children[key] as BasicModel<unknown>;
value[key] = model.getRawValue();
}
return value;
}
/**
* 获取 `FieldSet` 用于表单提交的值
*/
getSubmitValue() {
const value: any = {};
const childrenKeys = Object.keys(this.children);
for (let i = 0; i < childrenKeys.length; i++) {
const key = childrenKeys[i];
const model = this.children[key] as BasicModel<unknown>;
value[key] = model.getSubmitValue();
}
return this.normalizeBeforeSubmit(value);
}
/**
* 在 `FieldSet` 上注册一个新的字段。
* @param name 字段名
* @param model 字段对应的 model
*/
registerChild(name: string, model: BasicModel<any>) {
const children: UnknownFieldSetModelChildren = this.children;
const prev = children[name];
if (prev === model) {
return;
}
if (prev) {
this.removeChild(name);
}
this._subscribeChild(name, model);
model.owner = this;
children[name] = model;
this.childRegister$.next(name);
}
/**
* 在 `FieldSet` 上删除指定的字段。
* 返回被删除的 model。
* @param name 字段名
*/
removeChild<T extends keyof Children>(name: T): Children[T] | null {
if (hasOwnProperty(this.children, name)) {
const model = this.children[name];
model.owner = null;
this._unsubscribeChild(model);
delete this.children[name];
this.childRemove$.next(name as string);
return model;
}
return null;
}
dispose() {
super.dispose();
const { children } = this;
Object.keys(children).forEach(key => {
const child = children[key];
this._unsubscribeChild(child);
child.dispose();
});
// Close all subjects and setup sentinels to warn use after free errors
this.childRegister$.complete();
this.childRemove$.complete();
this._value$?.complete();
this._valid$?.complete();
this._valid$ = createSentinelSubject(this._displayName, false);
this._value$ = createSentinelSubject(
this._displayName,
{} as $FieldSetValue<Children>
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
(this.childRegister$ as Subject<string>) = createSentinelSubject(
this._displayName,
''
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
(this.childRemove$ as Subject<string>) = createSentinelSubject(
this._displayName,
''
);
}
/**
* 更新 `FieldSet` 的值
* @param value 待更新的值
*/
patchValue(value: Partial<$FieldSetValue<Children>>) {
if (!isPlainObject(value)) {
return;
}
this.patchedValue = value as $FieldSetValue<Children>;
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (hasOwnProperty(this.children, key)) {
this.children[key]?.patchValue(value[key]);
}
}
}
/**
* 清除 `FieldSet` 所有字段的值,同时清除 `initialValue`
*/
clear() {
const keys = Object.keys(this.children);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
this.children[key]?.clear();
}
}
/**
* 清除 `FieldSet` 所有字段的错误信息
*/
clearError() {
this.error$.next(null);
const children = this.children;
const keys = Object.keys(children);
const length = keys.length;
for (let i = 0; i < length; i++) {
const key = keys[i];
children[key]?.clearError();
}
}
/**
* 重置 `FieldValue` 所有字段的值,如果存在 `initialValue` 就是用初始值,否则使用默认值
*/
reset() {
const keys = Object.keys(this.children);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
this.children[key]?.reset();
}
}
/**
* 执行 `FieldSet` 的校验
* @param option 校验的参数
*/
validate(
option = ValidateOption.Default
): Promise<IMaybeError<any> | IMaybeError<any>[]> {
if (option & ValidateOption.IncludeChildrenRecursively) {
const childOption = option | ValidateOption.StopPropagation;
return Promise.all<IMaybeError<any>>(
Object.keys(this.children)
.map(key => this.children[key].validate(childOption))
.concat(this.triggerValidate(option))
);
}
return this.triggerValidate(option);
}
/**
* 是否 `FieldSet` 上的所有字段都没有被修改过
*/
pristine() {
const keys = Object.keys(this.children);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const child = this.children[key];
if (!child.pristine()) {
return false;
}
}
return true;
}
/**
* 是否 `FieldSet` 上有任意字段被修改过
*
* `dirty === !pristine`
*/
dirty() {
return !this.pristine();
}
/**
* 是否 `FieldSet` 上有任意字段被 touch 过
*
*/
touched() {
const keys = Object.keys(this.children);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
const child = this.children[key];
if (child.touched()) {
return true;
}
}
return false;
}
/**
* 返回指定字段名对应的 model
* @param name 字段名
*/
get<Name extends keyof Children>(
name: Name
): Children[Name] | undefined | null {
return this.children[name as string] as any;
}
private _setValid() {
this._valid$?.next(isNil(this.error) && !this.invalidModels.size);
}
private _initValue$() {
const value$ = new BehaviorSubject({} as $FieldSetValue<Children>);
this._value$ = value$;
for (const [name, model] of Object.entries(this.children)) {
this._subscribeChild(name, model);
}
const { childRegister$, childRemove$ } = this;
childRegister$.subscribe(name => {
value$.next({
...value$.value,
[name]: this.children[name].getRawValue(),
});
});
childRemove$.subscribe(name => {
value$.next(omit(value$.value, [name]) as $FieldSetValue<Children>);
});
}
private _initValid$() {
this._valid$ = new BehaviorSubject(isNil(this.error));
const $ = this.error$.subscribe(() => {
this._setValid();
});
this.mapModelToSubscriptions.set(this as BasicModel<unknown>, [$]);
for (const [name, model] of Object.entries(this.children)) {
this._subscribeChild(name, model);
}
}
/**
* Subscribe `valid$` and `value$` of the model
*/
private _subscribeChild(name: string, model: BasicModel<unknown>) {
const { invalidModels, _valid$, _value$ } = this;
if (_valid$) {
this._subscribeObservable(model, model._getValid$(), valid => {
if (valid) {
invalidModels.delete(model);
} else {
invalidModels.add(model);
}
this._setValid();
});
}
if (_value$) {
this._subscribeObservable(model, model._getValue$(), childValue => {
_value$.next({ ..._value$.value, [name]: childValue });
});
}
}
/**
* Unsubscribe `valid$` and `value$` of the model
* @param model
*/
private _unsubscribeChild(model: BasicModel<unknown>) {
const subs = this.mapModelToSubscriptions.get(model);
subs?.forEach(sub => sub.unsubscribe());
this.mapModelToSubscriptions.delete(model);
this.invalidModels.delete(model);
this._setValid();
}
/**
* Subscribe a specified observable of the model
* @param model as the key for mapping to subscription
* @param observable
* @param observer
*/
private _subscribeObservable<T>(
model: BasicModel<unknown>,
observable: Observable<T>,
observer: (value: T) => void
) {
const { mapModelToSubscriptions } = this;
const $ = observable.subscribe(observer);
const subs = mapModelToSubscriptions.get(model);
if (subs) {
subs.push($);
} else {
mapModelToSubscriptions.set(model, [$]);
}
}
}
FieldSetModel.prototype[SET_ID] = true;
export { FieldSetModel, $FieldSetValue };