65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular 实现创建具有依赖项的动态表单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (7投票s)

2020 年 4 月 17 日

CPOL

3分钟阅读

viewsIcon

14447

如何使用 JSON 模板动态渲染相互依赖的表单控件

引言

在本文中,我们将提出一个解决方案,演示如何使用 JSON 模板动态渲染彼此的值相互依赖的表单控件。

我的解决方案基于拥有一个抽象的表单控件,以及一个中心表单容器,因为每个控件都会在需要时向父表单推送通知,以便通知其依赖项。

Click to enlarge image

上面的类图显示

  • 一个表单由实现 IFormControl 接口的控件组成
  • 一个表单控件具有一个 ControlData 实例,用于渲染选项
  • 每个控件都应该引用表单内部的一个方法,该方法将用于接收来自该控件的消息,并将该消息广播给其依赖项。

基本场景

  • 用户编辑表单输入
  • 输入控件检查它是否有依赖项
  • 如果是
    • 将包含参数(控件名称和依赖项)的消息发送到表单
    • 表单处理调用并遍历所有控件,然后将消息转发给匹配依赖项名称的控件。

实现

我们将使用 Angular 框架和 typescript 语言来实现该解决方案,typescript 语言具有接口数据类型,这显然简化了实现并使其变得直接。

步骤 1

为数据创建接口。

export interface IControlData {
  controlName: string;
  controlType: string;
  placeholder?: string;
  dependents?: string[];
  order?: number;
  value?: any;
  DependentKey?: any;
  options?: Array<{
    optionName: string;
    value: string;
    dependentKey?: any;
  }>;
  validators?: {
    required?: boolean;
    minlength?: number;
    maxlength?: number;
  };
}

在上面的代码中,我们创建了创建表单控件所需的所有属性。

示例

  • controlName:用于保存每个 FormControl 的名称
  • Dependents:依赖于此 FormControl 的控件数组,必须根据业务逻辑在特定事件中通知它们

示例数据

export const FormData =  [
  {
    controlName: 'Name',
    controlType: 'text',
    valueType: 'text',
    dependents: ['Gender'],
    placeholder: 'Enter name'
  },
  {
    controlName: 'Type',
    placeholder: 'Select Type',
    controlType: 'radio',
    dependents: ['Email'],
    options: [{
      optionName: 'Type A',
      value: 'A'
    }, {
      optionName: 'Type B',
      value: 'B'
    }]
  },
  {
    controlName: 'Gender',
    placeholder: 'Select gender',
    controlType: 'select',
    dependents: ['Age', 'Books'],
    options: [{
      optionName: 'Male',
      value: 'male'
    }, {
      optionName: 'Female',
      value: 'female'
    }],
    validators: {
      required: true
    }
]

第二步

创建将由表单控件组件实现的接口。

export interface IControl {
  controlData: IControlData;
  sendMessage: EventEmitter<any>;

  doSomething(params: any): void;

}

在上面的代码中,我们创建了一个类型为 IControlData 的属性 controlData,用于保存每个控件的数据。

  • SendMessage:事件发射器,用于调用父组件(表单组件)中的一个函数,该函数将通过表单将携带的消息广播给依赖控件。
  • doSomething:将用于详细说明 FormControl 如何收到来自其他 FormControl 更改的通知并在其上执行操作。

步骤 3

为每种类型的表单控件创建一个组件。

示例

typescript 文件

export class DropdownComponentComponent implements IControl, OnInit {
  @Input() controlData: IControlData;
  @Output() sendMessage: EventEmitter<any>;

  constructor() {
    this.sendMessage = new EventEmitter<any>();
  }

  ngOnInit() {
  }

  onchange(val: any): void {
    this.controlData.value = val;
    this.sendMessage.emit({value: 'dropdown ' + this.controlData.controlName, 
                           controls: this.controlData.dependents});
  }

  doSomething(params: any) {
    // filter dropdown by id ;
    alert( this.controlData.controlName + ' received from ' + params);
  }
}

在上面的代码中,为下拉列表 formControl 创建了一个组件,该组件实现了 IControl 接口。

它使用“@Input”装饰器从父表单获取数据。

  • onChange:当控件值更改时触发 sendMessage 的函数,并将数据传递给父控件(动态表单),以便其依赖项收到此更改的通知。
  • doSomething:一个函数,仅发送一个警报,表明此控件收到了来自它所依赖的 FormControl 的通知。

Html. 文件应该如下所示

<select  [name]="controlData.controlName" [id]="controlData.controlName" 
(change)="onchange($event.target.value)">
  <option value="">{{controlData.placeholder}}</option>
  <option *ngFor="let option of controlData.options" 
   [value]="option.value">{{option.optionName}}</option>
</select>

这是下拉列表的实现,它展示了我们将如何使用 Input 数据(Control Data)来渲染下拉列表控件。

*可以重复相同的事情来创建其他类型的控件。

步骤 4

创建动态表单组件,该组件将所有这些控件组合在一个表单组中。

export class DynamicFormComponent implements OnInit {
  @Input() controlDataItems: IControlData[];
  @ViewChildren('control') controls: QueryList<IControl>;
  form: FormGroup;
  submitted: boolean;

  constructor() {
  }

  ngOnInit() {
    const formGroup = {};
    this.controlDataItems.forEach(formControl => {
      formGroup[formControl.controlName] = new FormControl('');
    });
    this.form = new FormGroup(formGroup);
  }

  sendMessageToControls(params: any): void {
    this.controls.forEach(control => {
      if (params.controls.indexOf(control.controlData.controlName) > -1) {
        control.doSomething(params.value);
      }
    });
  }

在上面的代码中,ngOnInit 函数循环遍历 formData,基于控件名称为每个控件创建一个 formControl,然后将它们添加到 formGroup 中。

  • @ViewChildren 控件将允许从视图 DOM 中获取元素或指令的 QueryList。 每次添加、删除或移动子元素时,查询列表将被更新,并且查询列表的可观察更改将发出一个新值 [Angular 文档]。

这意味着每次子组件发生更改时,父组件都会收到通知。

  • sendMessageToControls:一个由表单控件调用的函数,然后循环遍历控件以检查它是否依赖于任何其他子控件,从而调用“doSomething”方法以获得此更改的通知并在其上执行操作。

动态表单的 HTML 文件应该如下所示

<div class="form-group" [formGroup]="form" >
  <div *ngFor="let input of controlDataItems" class="form-row">

    <label [attr.for]="input.controlName">{{input.controlName}}</label>
    <div  [ngSwitch]="input.controlType" >

      <app-textbox-component *ngSwitchCase="'text'" 
       #control [controlData] ="input"  
       (sendMessage)="sendMessageToControls($event)" >
      </app-textbox-component>

      <app-dropdown-component #control  *ngSwitchCase="'select'" 
       [controlData] ="input" (sendMessage)="sendMessageToControls($event)">
      </app-dropdown-component>

      <app-radio-component *ngSwitchCase="'radio'" 
       #control [controlData] ="input" 
       (sendMessage)="sendMessageToControls($event)">
      </app-radio-component>

    </div>
  </div>

  <button type="submit" [disabled]="form.invalid" (click)="submitForm()">
    Submit
  </button>
</div>

在此 HTML 页面中,*ngFor 用于循环遍历控件数据集合以分别获取每个控件数据,然后使用 *ngSwitch 基于控件类型创建输入控件。

结论

现在我们可以轻松创建具有依赖组件的动态表单,其中每个控件都将动态添加其属性,并且彼此之间可以更轻松地进行通信。

参考文献

历史

  • 2020 年 4 月 17 日:初始版本
© . All rights reserved.