Angular 4.x 動態建立表單

本文將介紹如何動態建立表單組件,咱們最終實現的效果以下:html

圖片描述

在閱讀本文以前,請確保你已經掌握 Angular 響應式表單和動態建立組件的相關知識,若是對相關知識還不瞭解,推薦先閱讀一下 Angular 4.x Reactive FormsAngular 4.x 動態建立組件 這兩篇文章。對於已掌握的讀者,咱們直接進入主題。git

建立動態表單

建立 DynamicFormModule

在當前目錄先建立 dynamic-form 目錄,而後在該目錄下建立 dynamic-form.module.ts 文件,文件內容以下:github

dynamic-form/dynamic-form.module.tstypescript

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule
  ]
})
export class DynamicFormModule {}

建立完 DynamicFormModule 模塊,接着咱們須要在 AppModule 中導入該模塊:bootstrap

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { DynamicFormModule } from './dynamic-form/dynamic-form.module';

import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule, DynamicFormModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule { }

建立 DynamicForm 容器

進入 dynamic-form 目錄,在建立完 containers 目錄後,繼續建立 dynamic-form 目錄,而後在該目錄建立一個名爲 dynamic-form.component.ts 的文件,文件內容以下:segmentfault

import { Component, Input, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'dynamic-form',
  template: `
    <form [formGroup]="form">
    </form>
  `
})
export class DynamicFormComponent implements OnInit {
  @Input()
  config: any[] = [];

  form: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.form = this.createGroup();
  }

  createGroup() {
    const group = this.fb.group({});
    this.config.forEach(control => group.addControl(control.name, this.fb.control('')));
    return group;
  }
}

因爲咱們的表單是動態的,咱們須要接受一個數組類型的配置對象才能知道須要動態建立的內容。所以,咱們定義了一個 config 輸入屬性,用於接收數組類型的配置對象。數組

此外咱們利用了 Angular 響應式表單,提供的 API 動態的建立 FormGroup 對象。對於配置對象中的每一項,咱們要求該項至少包含兩個屬性,即 (type) 類型和 (name) 名稱:瀏覽器

  • type - 用於設置表單項的類型,如 inputselectbuttonapp

  • name - 用於設置表單控件的 name 屬性函數

createGroup() 方法中,咱們循環遍歷輸入的 config 屬性,而後利用 FormGroup 對象提供的 addControl() 方法,動態地添加新建的表單控件。

接下來咱們在 DynamicFormModule 模塊中聲明並導出新建的 DynamicFormComponent 組件:

import { DynamicFormComponent } from './containers/dynamic-form/dynamic-form.component';

@NgModule({
  imports: [
    CommonModule,
    ReactiveFormsModule
  ],
  declarations: [
    DynamicFormComponent
  ],
  exports: [
    DynamicFormComponent
  ]
})
export class DynamicFormModule {}

如今咱們已經建立了表單,讓咱們實際使用它。

使用動態表單

打開 app.component.ts 文件,在組件模板中引入咱們建立的 dynamic-form 組件,並設置相關的配置對象,具體示例以下:

app.component.ts

import { Component } from '@angular/core';

interface FormItemOption {
  type: string;
  label: string;
  name: string;
  placeholder?: string;
  options?: string[]
}

@Component({
  selector: 'exe-app',
  template: `
   <div>
     <dynamic-form [config]="config"></dynamic-form>
   </div>
  `
})
export class AppComponent {
  config: FormItemOption[] = [
    {
      type: 'input',
      label: 'Full name',
      name: 'name',
      placeholder: 'Enter your name'
    },
    {
      type: 'select',
      label: 'Favourite food',
      name: 'food',
      options: ['Pizza', 'Hot Dogs', 'Knakworstje', 'Coffee'],
      placeholder: 'Select an option'
    },
    {
      type: 'button',
      label: 'Submit',
      name: 'submit'
    }
  ];
}

上面代碼中,咱們在 AppComponent 組件類中設置了 config 配置對象,該配置對象中設置了三種類型的表單類型。對於每一個表單項的配置對象,咱們定義了一個 FormItemOption 數據接口,該接口中咱們定義了三個必選屬性:type、label 和 name 及兩個可選屬性:options 和 placeholder。下面讓咱們建立對應類型的組件。

自定義表單項組件

FormInputComponent

dynamic-form 目錄,咱們新建一個 components 目錄,而後建立 form-inputform-selectform-button 三個文件夾。建立完文件夾後,咱們先來定義 form-input 組件:

form-input.component.ts

import { Component, ViewContainerRef } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
    selector: 'form-input',
    template: `
    <div [formGroup]="group">
      <label>{{ config.label }}</label>
      <input
        type="text"
        [attr.placeholder]="config.placeholder"
        [formControlName]="config.name" />
    </div>
  `
})
export class FormInputComponent {
    config: any;
    group: FormGroup;
}

上面代碼中,咱們在 FormInputComponent 組件類中定義了 configgroup 兩個屬性,但咱們並無使用 @Input 裝飾器來定義它們,由於咱們不會以傳統的方式來使用這個組件。接下來,咱們來定義 selectbutton 組件。

FormSelectComponent

import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'form-select',
  template: `
    <div [formGroup]="group">
      <label>{{ config.label }}</label>
      <select [formControlName]="config.name">
        <option value="">{{ config.placeholder }}</option>
        <option *ngFor="let option of config.options">
          {{ option }}
        </option>
      </select>
    </div>
  `
})
export class FormSelectComponent {
  config: Object;
  group: FormGroup;
}

FormSelectComponent 組件與 FormInputComponent 組件的主要區別是,咱們須要循環配置中定義的options屬性。這用於向用戶顯示全部的選項,咱們還使用佔位符屬性,做爲默認的選項。

FormButtonComponent

import { Component } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'form-button',
  template: `
    <div [formGroup]="group">
      <button type="submit">
        {{ config.label }}
      </button>
    </div>
  `
})
export class FormButtonComponent{
  config: Object;
  group: FormGroup;
}

以上代碼,咱們只是定義了一個簡單的按鈕,它使用 config.label 的值做爲按鈕文本。與全部組件同樣,咱們須要在前面建立的模塊中聲明這些自定義組件。打開 dynamic-form.module.ts 文件並添加相應聲明:

// ...
import { FormButtonComponent } from './components/form-button/form-button.component';
import { FormInputComponent } from './components/form-input/form-input.component';
import { FormSelectComponent } from './components/form-select/form-select.component';

@NgModule({
  // ...
  declarations: [
    DynamicFormComponent,
    FormButtonComponent,
    FormInputComponent,
    FormSelectComponent
  ],
  exports: [
    DynamicFormComponent
  ]
})
export class DynamicFormModule {}

到目前爲止,咱們已經建立了三個組件。若想動態的建立這三個組件,咱們將定義一個指令,該指令的功能跟 router-outlet 指令相似。接下來在 components 目錄內部,咱們新建一個 dynamic-field 目錄,而後建立 dynamic-field.directive.ts 文件。該文件的內容以下:

import { Directive, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Directive({
  selector: '[dynamicField]'
})
export class DynamicFieldDirective {
  @Input()
  config: Object;

  @Input()
  group: FormGroup;
}

咱們將指令的 selector 屬性設置爲 [dynamicField],由於咱們將其應用爲屬性而不是元素。

這樣作的好處是,咱們的指令能夠應用在 Angular 內置的 <ng-container> 指令上。<ng-container> 是一個邏輯容器,可用於對節點進行分組,但不做爲 DOM 樹中的節點,它將被渲染爲 HTML中的 comment 元素。所以配合 <ng-container> 指令,咱們只會在 DOM 中看到咱們自定義的組件,而不會看到 <dynamic-field> 元素 (由於 DynamicFieldDirective 指令的 selector 被設置爲 [dynamicField] )。

另外在指令中,咱們使用 @Input 裝飾器定義了兩個輸入屬性,用於動態設置 configgroup 對象。接下來咱們開始動態渲染組件。

動態渲染組件,咱們須要用到 ComponentFactoryResolverViewContainerRef 兩個對象。ComponentFactoryResolver 對象用於建立對應類型的組件工廠 (ComponentFactory),而 ViewContainerRef 對象用於表示一個視圖容器,可添加一個或多個視圖,經過它咱們能夠方便地建立和管理內嵌視圖或組件視圖。

讓咱們在 DynamicFieldDirective 指令構造函數中,注入相關對象,具體代碼以下:

import { ComponentFactoryResolver, Directive, Input, OnInit, 
        ViewContainerRef } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Directive({
  selector: '[dynamicField]'
})
export class DynamicFieldDirective implements OnInit {
  @Input()
  config;

  @Input()
  group: FormGroup;
  
  constructor(
    private resolver: ComponentFactoryResolver,
    private container: ViewContainerRef
  ) {}
  
  ngOnInit() {
    
  }
}

上面代碼中,咱們還添加了 ngOnInit 生命週期鉤子。因爲咱們容許使用 inputselect 類型來聲明組件的類型,所以咱們須要建立一個對象來將字符串映射到相關的組件類,具體以下:

// ...
import { FormButtonComponent } from '../form-button/form-button.component';
import { FormInputComponent } from '../form-input/form-input.component';
import { FormSelectComponent } from '../form-select/form-select.component';

const components = {
  button: FormButtonComponent,
  input: FormInputComponent,
  select: FormSelectComponent
};

@Directive(...)
export class DynamicFieldDirective implements OnInit {
  // ...
}

這將容許咱們經過 components['button'] 獲取對應的 FormButtonComponent 組件類,而後咱們能夠把它傳遞給 ComponentFactoryResolver 對象以獲取對應的 ComponentFactory (組件工廠):

// ...
const components = {
  button: FormButtonComponent,
  input: FormInputComponent,
  select: FormSelectComponent
};

@Directive(...)
export class DynamicFieldDirective implements OnInit {
  // ...
  ngOnInit() {
    const component = components[this.config.type];
    const factory = this.resolver.resolveComponentFactory<any>(component);
  }
  // ...
}

如今咱們引用了配置中定義的給定類型的組件,並將其傳遞給 ComponentFactoryRsolver 對象提供的resolveComponentFactory() 方法。您可能已經注意到咱們在 resolveComponentFactory 旁邊使用了 <any>,這是由於咱們要建立不一樣類型的組件。此外咱們也能夠定義一個接口,而後每一個組件都去實現,若是這樣的話 any 就能夠替換成咱們已定義的接口。

如今咱們已經有了組件工廠,咱們能夠簡單地告訴咱們的 ViewContainerRef 爲咱們建立這個組件:

@Directive(...)
export class DynamicFieldDirective implements OnInit {
  // ...
  component: any;
  
  ngOnInit() {
    const component = components[this.config.type];
    const factory = this.resolver.resolveComponentFactory<any>(component);
    this.component = this.container.createComponent(factory);
  }
  // ...
}

咱們如今已經能夠將 configgroup 傳遞到咱們動態建立的組件中。咱們能夠經過 this.component.instance 訪問到組件類的實例:

@Directive(...)
export class DynamicFieldDirective implements OnInit {
  // ...
  component;
  
  ngOnInit() {
    const component = components[this.config.type];
    const factory = this.resolver.resolveComponentFactory<any>(component);
    this.component = this.container.createComponent(factory);
    this.component.instance.config = this.config;
    this.component.instance.group = this.group;
  }
  // ...
}

接下來,讓咱們在 DynamicFormModule 中聲明已建立的 DynamicFieldDirective 指令:

// ...
import { DynamicFieldDirective } from './components/dynamic-field/dynamic-field.directive';

@NgModule({
  // ...
  declarations: [
    DynamicFieldDirective,
    DynamicFormComponent,
    FormButtonComponent,
    FormInputComponent,
    FormSelectComponent
  ],
  exports: [
    DynamicFormComponent
  ]
})
export class DynamicFormModule {}

若是咱們直接在瀏覽器中運行以上程序,控制檯會拋出異常。當咱們想要經過 ComponentFactoryResolver 對象動態建立組件的話,咱們須要在 @NgModule 配置對象的一個屬性 - entryComponents 中,聲明需動態加載的組件。

@NgModule({
  // ...
  entryComponents: [
    FormButtonComponent,
    FormInputComponent,
    FormSelectComponent
  ]
})
export class DynamicFormModule {}

基本工做都已經完成,如今咱們須要作的就是更新 DynamicFormComponent 組件,應用咱們以前已經 DynamicFieldDirective 實現動態組件的建立:

@Component({
  selector: 'dynamic-form',
  template: `
    <form
      class="dynamic-form"
      [formGroup]="form">
      <ng-container
        *ngFor="let field of config;"
        dynamicField
        [config]="field"
        [group]="form">
      </ng-container>
    </form>
  `
})
export class DynamicFormComponent implements OnInit {
  // ...
}

正如咱們前面提到的,咱們使用 <ng-container>做爲容器來重複咱們的動態字段。當咱們的組件被渲染時,這是不可見的,這意味着咱們只會在 DOM 中看到咱們的動態建立的組件。

此外咱們使用 *ngFor 結構指令,根據 config (數組配置項) 動態建立組件,並設置 dynamicField 指令的兩個輸入屬性:config 和 group。最後咱們須要作的是實現表單提交功能。

表單提交

咱們須要作的是爲咱們的 <form> 組件添加一個 (ngSubmit) 事件的處理程序,並在咱們的動態表單組件中新增一個 @Output 輸出屬性,以便咱們能夠通知使用它的組件。

import { Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';

@Component({
  selector: 'dynamic-form',
  template: `
    <form 
      [formGroup]="form"
      (ngSubmit)="submitted.emit(form.value)">
      <ng-container
       *ngFor="let field of config;"
       dynamicField
       [config]="field"
       [group]="form">
      </ng-container>
    </form>
  `
})
export class DynamicFormComponent implements OnInit {
  @Input() config: any[] = [];

  @Output() submitted: EventEmitter<any> = new EventEmitter<any>();
  // ...
}

最後咱們同步更新一下 app.component.ts 文件:

import { Component } from '@angular/core';

@Component({
  selector: 'exe-app',
  template: `
    <div class="app">
      <dynamic-form 
        [config]="config"
        (submitted)="formSubmitted($event)">
      </dynamic-form>
    </div>
  `
})
export class AppComponent {
  // ...
  formSubmitted(value: any) {
    console.log(value);
  }
}

Toddmotto 大神線上示例 - angular-dynamic-forms,查看完整代碼請訪問 - toddmott/angular-dynamic-forms

我有話說

在自定義表單控件組件中 [formGroup]="group" 是必須的麼?

form-input.component.ts

<div [formGroup]="group">
  <label>{{ config.label }}</label>
  <input
     type="text"
     [attr.placeholder]="config.placeholder"
     [formControlName]="config.name" />
</div>

若是去掉 <div> 元素上的 [formGroup]="group" 屬性,從新編譯後瀏覽器控制檯將會拋出如下異常:

Error: formControlName must be used with a parent formGroup directive.  You'll want to add a formGroup directive and pass it an existing FormGroup instance (you can create one in your class).
Example:

<div [formGroup]="myGroup">
  <input formControlName="firstName">
</div>

In your class:
this.myGroup = new FormGroup({
  firstName: new FormControl()
});

formControlName 指令中,初始化控件的時候,會驗證父級指令的類型:

private _checkParentType(): void {
    if (!(this._parent instanceof FormGroupName) &&
        this._parent instanceof AbstractFormGroupDirective) {
      ReactiveErrors.ngModelGroupException();
    } else if (
        !(this._parent instanceof FormGroupName) && 
        !(this._parent instanceof FormGroupDirective) &&
        !(this._parent instanceof FormArrayName)) {
      ReactiveErrors.controlParentException();
    }
  }

那爲何要驗證,是由於要把新增的控件添加到對應 formDirective 對象中:

private _setUpControl() {
    this._checkParentType();
    this._control = this.formDirective.addControl(this);
    if (this.control.disabled && this.valueAccessor !.setDisabledState) {
      this.valueAccessor !.setDisabledState !(true);
    }
    this._added = true;
}

參考資源

相關文章
相關標籤/搜索