delon
@delon/form 是 delon 包中的一个模块,主要提供了 angular
相关的动态表单的内容,这篇文章是来大概的看一下 sf
组件如何实现的
从 SFComponent 开始
<ng-template #con> <ng-content></ng-content> </ng-template> <form nz-form [nzLayout]="layout" (submit)="onSubmit($event)" </form>
1-3 定义了一个模版 唯一标识符为 con
,后面使用了 nz-form
这是 ng-zorro
封装下的 angular
表单,传入 layout
布局和 submit
表单提交函数
<sf-item *ngIf="rootProperty" [formProperty]="rootProperty"></sf-item>
在向下是 sf-item
组件
let nextUniqueId = 0;
首先是一个不唯一的 id
,应该是用来区分表单元素名称的
@Component({ selector: 'sf-item', exportAs: 'sfItem', host: { '[class.sf__item]': 'true' }, template: ` <ng-template #target></ng-template> `, preserveWhitespaces: false, encapsulation: ViewEncapsulation.None })
可以看到 html 仅仅为 <ng-template>
并给了一个标识符为 target
,preserveWhitespaces
编译器移除所哟不必要的空格选项为 false,ViewEncapsulation
无 Shadow DOM,并且也无样式包装。
export class SFItemComponent implements OnInit, OnChanges, OnDestroy
实现了三个生命周期,一个初始化一个销毁,还有一个父组件输入的值发生变化就会调用的 OnChanges
private ref: ComponentRef<Widget<FormProperty, SFUISchemaItem>>; readonly unsubscribe$ = new Subject<void>(); widget: Widget<FormProperty, SFUISchemaItem> | null = null; @Input() formProperty: FormProperty; @ViewChild('target', { read: ViewContainerRef, static: true }) container: ViewContainerRef;
ref 类型为 Widget
组件的引用,unsubscribe$ 负责保存需要取消订阅的值,formProperty
为父组件输入的值,还有一个 container
拿到了标识着 target
视图元素的引用
ngOnInit(): void { this.terminator.onDestroy.subscribe(() => this.ngOnDestroy()); }
onInit 函数订阅了 销毁 topic 的信息,当销毁 topic 的数据发生变化时,触发本 sf-item
的 onDestory 函数,主要是用来在外部控制 sf-item
销毁用的
ngOnChanges(): void { const p = this.formProperty; this.ref = this.widgetFactory.createWidget(this.container, (p.ui.widget || p.schema.type) as string); this.onWidgetInstanciated(this.ref.instance); }
输入值发生变化调用 ngOnChanges
,在 container
也就是 target
标识的容器的地方 createWidget
将控件加载到该位置。然后在控件被创建完成以后,调用 onWidgetInstanciated
初始化挂载进来的控件
onWidgetInstanciated(widget: Widget<FormProperty, SFUISchemaItem>): void { this.widget = widget; const id = `_sf-${nextUniqueId++}`; const ui = this.formProperty.ui as SFUISchemaItem; this.widget.formProperty = this.formProperty; this.widget.schema = this.formProperty.schema; this.widget.ui = ui; this.widget.id = id; this.widget.firstVisual = ui.firstVisual as boolean; this.formProperty.widget = widget; }
主要还是得给一个唯一 id
和 schema
ngOnDestroy(): void { const { unsubscribe$ } = this; unsubscribe$.next(); unsubscribe$.complete(); this.ref.destroy(); }
sf-item
负责渲然表单控件,这就是动态表单的入口了,其中比较重要的函数是
this.widgetFactory.createWidget(this.container, (p.ui.widget || p.schema.type) as string);
通过 json对象,也就是formProperty
来动态创建控件,稍后再说,回到 sf
下看后面写了些什么
<ng-container *ngIf="button !== 'none'; else con"> <nz-form-item *ngIf="_btn.render" [ngClass]="_btn.render!.class!" class="sf-btns" [fixed-label]="_btn.render!.spanLabelFixed!" > <div nz-col class="ant-form-item-control" [nzSpan]="btnGrid.span" [nzOffset]="btnGrid.offset" [nzXs]="btnGrid.xs" [nzSm]="btnGrid.sm" [nzMd]="btnGrid.md" [nzLg]="btnGrid.lg" [nzXl]="btnGrid.xl" [nzXXl]="btnGrid.xxl" > <div class="ant-form-item-control-input"> <div class="ant-form-item-control-input-content"> <ng-container *ngIf="button; else con"> <button type="submit" nz-button data-type="submit" [nzType]="_btn.submit_type!" [nzSize]="_btn.render!.size!" [nzLoading]="loading" [disabled]="liveValidate && !valid" > <i *ngIf="_btn.submit_icon" nz-icon [nzType]="_btn.submit_icon.type!" [nzTheme]="_btn.submit_icon.theme!" [nzTwotoneColor]="_btn.submit_icon.twoToneColor!" [nzIconfont]="_btn.submit_icon.iconfont!" ></i> {{ _btn.submit }} </button> <button *ngIf="_btn.reset" type="button" nz-button data-type="reset" [nzType]="_btn.reset_type!" [nzSize]="_btn.render!.size!" [disabled]="loading" (click)="reset(true)" > <i *ngIf="_btn.reset_icon" nz-icon [nzType]="_btn.reset_icon.type!" [nzTheme]="_btn.reset_icon.theme!" [nzTwotoneColor]="_btn.reset_icon.twoToneColor!" [nzIconfont]="_btn.reset_icon.iconfont!" ></i> {{ _btn.reset }} </button> </ng-container> </div> </div> </div> </nz-form-item> </ng-container>
这里写了两个 button
,保存/重置,假如 button
参数不为空,那么直接显示两个按钮,假如 button
参数为空,那么 con标识的template
,其实,这么长,只是在控制按钮显示/隐藏
在看 Widget
之前,有几个抽象类和接口需要提前看一下
export abstract class FormProperty {}
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: Record<string, unknown>; _value: SFValue = null; widget: Widget<FormProperty, SFUISchemaItem>; path: string;
错误提示,值变更,错误变更,是否可见,schema 信息,ui 信息,formData 数据等。。。。
export abstract class PropertyGroup extends FormProperty { properties: { [key: string]: FormProperty } | FormProperty[] | null = null;
可以看出 PropertyGroup
和 FormProperty
实际上就是在扩展 JSONSchema
,是一个树形结构,与此同时,JSONSchema
的几个基础数据类型也都继承了 PropertyGroup
以下为图示,这俩 class 扩展了 JSONSchema
好,下面在看一下 FormPropertyFactory
核心的函数是 createProperty
createProperty( // 接收jsonSchema schema: SFSchema, // 接收uiSchema ui: SFUISchema | SFUISchemaItem, // 数据 formData: Record<string, unknown>, // 父节点是谁 parent: PropertyGroup | null = null, // property名称 propertyId?: string ): FormProperty { let newProperty: FormProperty | null = null; // 当前这个元素在json里的路径是什么,就好像deepGet(path)的这个path一样 let path = ''; if (parent) { // 首先把父节点的属性路径加上 path += parent.path; if (parent.parent !== null) { path += SF_SEQ; } // 看看是object还是array switch (parent.type) { case 'object': // 如果是object,那么直接附加属性id path += propertyId; break; case 'array': // 如果是array,那加入数组的长度 path += ((parent as ArrayProperty).properties as PropertyGroup[]).length; break; default: throw new Error(`Instanciation of a FormProperty with an unknown parent type: ${parent.type}`); } } else { path = SF_SEQ; } // JOSONSchema引用类型是可以引用预先定义好的其他JSON的,这里就是处理ref类型的json if (schema.$ref) { const refSchema = retrieveSchema(schema, parent!.root.schema.definitions); newProperty = this.createProperty(refSchema, ui, formData, parent, path); } else { // fix required // 通过判断JSONSchema的required列表控制ui是否是必填类型 if ( (propertyId && parent!.schema.required!.indexOf(propertyId.split(SF_SEQ).pop()!) !== -1) || ui.showRequired === true ) { ui._required = true; } // fix title // 如果没有title那么默认使用自增id if (schema.title == null) { schema.title = propertyId; } // fix date // 当json的类型为string和number时,需要对数据进行判断,如果json下的uiJSON中传入的widget为时间/日期类型那么需要做转换 if ((schema.type === 'string' || schema.type === 'number') && !schema.format && !(ui as SFUISchemaItem).format) { if ((ui as SFUISchemaItem).widget === 'date') ui._format = schema.type === 'string' ? this.options.uiDateStringFormat : this.options.uiDateNumberFormat; else if ((ui as SFUISchemaItem).widget === 'time') ui._format = schema.type === 'string' ? this.options.uiTimeStringFormat : this.options.uiTimeNumberFormat; } else { ui._format = ui.format; } // 然后是通过jsonSchema的具体类型来分发创建不同类型的Property switch (schema.type) { case 'integer': case 'number': newProperty = new NumberProperty( this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options ); break; case 'string': newProperty = new StringProperty( this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options ); break; case 'boolean': newProperty = new BooleanProperty( this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options ); break; case 'object': newProperty = new ObjectProperty( this, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options ); break; case 'array': newProperty = new ArrayProperty( this, this.schemaValidatorFactory, schema, ui, formData, parent, path, this.options ); break; default: throw new TypeError(`Undefined type ${schema.type}`); } } // 最后调用`initializeRoot`加载json if (newProperty instanceof PropertyGroup) { this.initializeRoot(newProperty); } return newProperty; }
这个函数只做了一件事,就是设置可见状态了
private initializeRoot(rootProperty: PropertyGroup): void { // rootProperty.init(); rootProperty._bindVisibility(); }
property 相关的就是拿到 jsonSchema 做一些处理,赋值给扩展属性。真正渲染的时候依靠的是对 Widgets 的判断。
@Directive() export abstract class Widget<T extends FormProperty, UIT extends SFUISchemaItem> implements AfterViewInit { formProperty: T; error: string; showError = false; id = ''; schema: SFSchema; ui: UIT; firstVisual = false; @HostBinding('class') get cls(): NgClassType { return this.ui.class || ''; } get disabled(): boolean { if (this.schema.readOnly === true || this.sfComp!.disabled) { return true; } return false; } get l(): LocaleData { return this.formProperty.root.widget.sfComp!.locale; } get oh(): SFOptionalHelp { return this.ui.optionalHelp as SFOptionalHelp; } get dom(): DomSanitizer { return this.injector.get(DomSanitizer); } get cleanValue(): boolean { return this.sfComp?.cleanValue!; } constructor( @Inject(ChangeDetectorRef) public readonly cd: ChangeDetectorRef, @Inject(Injector) public readonly injector: Injector, @Inject(SFItemComponent) public readonly sfItemComp?: SFItemComponent, @Inject(SFComponent) public readonly sfComp?: SFComponent ) {} ngAfterViewInit(): void { this.formProperty.errorsChanges .pipe(takeUntil(this.sfItemComp!.unsubscribe$)) .subscribe((errors: ErrorData[] | null) => { if (errors == null) return; di(this.ui, 'errorsChanges', this.formProperty.path, errors); // 不显示首次校验视觉 if (this.firstVisual) { this.showError = errors.length > 0; this.error = this.showError ? (errors[0].message as string) : ''; this.cd.detectChanges(); } this.firstVisual = true; }); this.afterViewInit(); } setValue(value: SFValue): void { this.formProperty.setValue(value, false); di(this.ui, 'valueChanges', this.formProperty.path, this.formProperty); } get value(): NzSafeAny { return this.formProperty.value; } detectChanges(onlySelf: boolean = false): void { if (onlySelf) { this.cd.markForCheck(); } else { this.formProperty.root.widget.cd.markForCheck(); } } abstract reset(value: SFValue): void; abstract afterViewInit(): void; }
可以看到,每个 Widget 都要传入范型,T 为继承自 FormProperty 的,UIT 继承自 SFUISchemaItem,这样就将不同的控件和不同的属性(JSONSchema)结合了起来,并赋值给了 fromProperty
,ui
这两个属性,注意,这里标注的是 @Directive
是一个 angular 指令
继续向下看
@Directive() export class ControlWidget extends Widget<FormProperty, SFUISchemaItem> { reset(_value: SFValue): void {} afterViewInit(): void {} } @Directive() export class ControlUIWidget<UIT extends SFUISchemaItem> extends Widget<FormProperty, UIT> { reset(_value: SFValue): void {} afterViewInit(): void {} } @Directive() export class ArrayLayoutWidget extends Widget<ArrayProperty, SFArrayWidgetSchema> implements AfterViewInit { reset(_value: SFValue): void {} afterViewInit(): void {} ngAfterViewInit(): void { this.formProperty.errorsChanges .pipe(takeUntil(this.sfItemComp!.unsubscribe$)) .subscribe(() => this.cd.detectChanges()); } } @Directive() export class ObjectLayoutWidget extends Widget<ObjectProperty, SFObjectWidgetSchema> implements AfterViewInit { reset(_value: SFValue): void {} afterViewInit(): void {} ngAfterViewInit(): void { this.formProperty.errorsChanges .pipe(takeUntil(this.sfItemComp!.unsubscribe$)) .subscribe(() => this.cd.detectChanges()); } }
抽象类型的 Widget
具体衍生出了以上几种类型,ControlWidget
,ControlUIWidget
,ArrayLayoutWidget
,ObjectLayoutWidget
一种是常规控件,第二种是可扩展 UI 的控件,第三种是数组控件,第四种是对象控件
以上是对 Widget
控件层面的一些类型,创建控件还需要使用 WidgetFactory
来进行真正的创建
export class WidgetRegistry { private _widgets: { [type: string]: Widget<FormProperty, SFUISchemaItem> } = {}; private defaultWidget: Widget<FormProperty, SFUISchemaItem>; get widgets(): { [type: string]: Widget<FormProperty, SFUISchemaItem> } { return this._widgets; } setDefault(widget: NzSafeAny): void { this.defaultWidget = widget; } register(type: string, widget: NzSafeAny): void { this._widgets[type] = widget; } has(type: string): boolean { return this._widgets.hasOwnProperty(type); } getType(type: string): Widget<FormProperty, SFUISchemaItem> { if (this.has(type)) { return this._widgets[type]; } return this.defaultWidget; }
上面是 Widget注册表
类,存储了一份字符串-> 控件的映射,这里的字符串主要是用来对应 JSONSchema 的 type 字段,表示 json 中的 type 将要渲染成哪个具体的组件,映射关系在 nz-widget.registry.ts
中
export class NzWidgetRegistry extends WidgetRegistry { constructor() { super(); this.register('object', ObjectWidget); this.register('array', ArrayWidget); this.register('text', TextWidget); this.register('string', StringWidget); this.register('number', NumberWidget); this.register('integer', NumberWidget); this.register('date', DateWidget); this.register('time', TimeWidget); this.register('radio', RadioWidget); this.register('checkbox', CheckboxWidget); this.register('boolean', BooleanWidget); this.register('textarea', TextareaWidget); this.register('select', SelectWidget); this.register('tree-select', TreeSelectWidget); this.register('tag', TagWidget); this.register('upload', UploadWidget); this.register('transfer', TransferWidget); this.register('slider', SliderWidget); this.register('rate', RateWidget); this.register('autocomplete', AutoCompleteWidget); this.register('cascader', CascaderWidget); this.register('mention', MentionWidget); this.register('custom', CustomWidget); this.setDefault(StringWidget); } }
@Injectable() export class WidgetFactory { constructor(private registry: WidgetRegistry, private resolver: ComponentFactoryResolver) {} createWidget(container: ViewContainerRef, type: string): ComponentRef<Widget<FormProperty, SFUISchemaItem>> { if (!this.registry.has(type)) { console.warn(`No widget for type "${type}"`); } const componentClass = this.registry.getType(type) as NzSafeAny; const componentFactory = this.resolver.resolveComponentFactory<Widget<FormProperty, SFUISchemaItem>>(componentClass); return container.createComponent(componentFactory); } }
通过 createWidget
从注册表中检索 Widget
,进而将具体的 Component
刷到 Container
容器中,完成控件的渲染
再次回到 SFComponent
图中不涉及到 UI 的 JSON,因为没有地方画了
从 sf
的 onInit 开始看,可以看到最终调用了 refreshSchema 重新构建出了 rootProperties 此时 json 对象已经被扩展了,然后 sf-item
通过检测到 json 变化从而调用 widgetFactory
将 json.type
类型的控件 create 出来
自定义 Widget
需要继承,通常是继承 ControlUIWidget
实现其函数,并且注入 WidgetRegister
将 type
作为 key 注册到注册表里即可
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于