@delon/form

  |   0 评论   |   0 浏览

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> 并给了一个标识符为 targetpreserveWhitespaces 编译器移除所哟不必要的空格选项为 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;
  }

主要还是得给一个唯一 idschema

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;

可以看出 PropertyGroupFormProperty 实际上就是在扩展 JSONSchema,是一个树形结构,与此同时,JSONSchema 的几个基础数据类型也都继承了 PropertyGroup 以下为图示,这俩 class 扩展了 JSONSchema

image.png

好,下面在看一下 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

image.png

图中不涉及到 UI 的 JSON,因为没有地方画了

sf 的 onInit 开始看,可以看到最终调用了 refreshSchema 重新构建出了 rootProperties 此时 JSON 对象已经被扩展了,然后 sf-item 通过检测到 JSON 变化从而调用 widgetFactoryjson.type 类型的控件 create 出来

自定义 Widget

关于一些 angular 内容

export abstract class ChangeDetectorRef {
  abstract markForCheck(): void;
  abstract detach(): void;
  abstract detectChanges(): void;
  abstract reattach(): void;
}

markForCheck() - 当输入已更改或视图中发生了事件时,组件通常会标记为脏的(需要重新渲染)。调用此方法会确保即使那些触发器没有被触发,也仍然检查该组件。<br>在组件的 metadata 中如果设置了 changeDetection: ChangeDetectionStrategy.OnPush 条件,那么变化检测不会再次执行,除非手动调用该方法。
detach() - 从变化检测树中分离变化检测器,该组件的变化检测器将不再执行变化检测,除非手动调用 reattach() 方法。
reattach() - 重新添加已分离的变化检测器,使得该组件及其子组件都能执行变化检测
detectChanges() - 从该组件到各个子组件执行一次变化检测 检查该视图及其子视图。与 <a href="https://angular.cn/api/core/ChangeDetectorRef#detach">detach</a> 结合使用可以实现局部变更检测。