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 注册到注册表里即可
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于