如何在角度建立动态形式
#javascript #前端 #forms #angular

当我们构建Angular应用程序时,形式的创建和构建是手动过程。小型更改的维护,例如添加新字段或从输入切换字段类型到日期字段应该很容易,但是为什么不创建表单以灵活并响应业务更改?

这些更改需要我们再次触摸表单,以更新硬编码的表单声明和字段。为什么不更改以使我们的形式动态而不是硬编码?

今天,我们将学习如何使用模型中的反应性形式创建形式,并将它们绑定到模板动态中的输入,无线电和复选框。

我知道代码需要类型和接口,但我想制作可管理的文章

设想

我们为营销团队工作,该团队希望一份表格要求用户的名称,姓氏和年龄。让我们构建它。

首先,声明FormGroup字段registerForm并创建方法buildForm()以手动添加formGroup中的每个字段。

import { Component, OnInit, VERSION } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  registerForm: FormGroup;

  ngOnInit() {
    this.buildForm();
  }

  buildForm() {
    this.registerForm = new FormGroup({
      name: new FormControl(''),
      lastName: new FormControl(''),
      age: new FormControl(''),
    });
  }
}

使用[formGroup]指令链接的输入添加HTML标记。

<h1>Register</h1>
<form [formGroup]="registerForm">
  <label>Name:<input type="text" formControlName="name" /></label>
  <label>LastName: <input type="text" formControlName="lastName" /></label>
  <label> Age: <input type="number" formControlName="age" /></label>
</form>

最后,我们有静态形式!

one

但是明天,营销希望向新闻通讯请求地址或复选框。我们需要更新表单声明,添加formControl和输入,并且您知道整个过程。

我们希望避免每次重复相同的任务。我们需要打开表单对动态和对业务变化做出反应,而无需再次触摸HTML或Typescript文件。

创建动态formcontrol和输入

首先,我们将有两个任务来构建我们的表单动态。

  • 从业务对象创建表单组。
  • 显示以形式渲染的字段列表。

首先,将registerModel重命名为model并声明字段数组。它将具有我们动态形式的输入模型的所有元素:

fields: [];
model = {
        name: '',
        lastName: '',
        address: '',
        age: '',
 };

接下来,使用theformGroupFields对象和
创建方法getFormControlFields() 使用for迭代模型中的所有属性,以推入formGroupFields。将每个字段添加到fields阵列中。

代码看起来像这样:

 getFormControlsFields() {
        const formGroupFields = {};
        for (const field of Object.keys(this.model)) {
            formGroupFields[field] = new FormControl("");
            this.fields.push(field);
        }
        return formGroupFields;
    }

buildForm()方法中,添加带有getFormControlFields()值的变量formGroupFields,并将formGroupFields分配给registerForm

 buildForm() {
    const formGroupFields = this.getFormControlsFields();
    this.registerForm = new FormGroup(formGroupFields);
  }

接下来,使用*ngFor指令在fields阵列上迭代。使用变量field显示标签并设置具有相同字段值的输入[formControlName]。

<form [formGroup]="registerForm">
    <div *ngFor="let field of fields">
           <label>{{field}}</label>
    <input type="text" [formControlName]="field"/>
</form>

保存更改,我们从定义中动态生成相同的形式。

one

,但这只是开始。我们希望分解一些责任,以使我们能够使这种形式灵活地改变而不会痛苦。

分开表单过程和fieldType

app.component执行了一些任务,创建模型,表单和渲染输入。让我们稍微清理一下。

创建具有输入属性的dynamic-form.component,以使模型生成,并将registerForm更新为dynamicFormGroup。将功能buildFormgetFormControlsFields移动到动态。

import { Component, Input, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";

@Component({
  selector: "app-dynamic-form",
  templateUrl: "./dynamic-form.component.html",
  styleUrls: ["./dynamic-form.component.css"],
})
export class DynamicFormComponent implements OnInit {
  dynamicFormGroup: FormGroup;
  @Input() model: {};

  fields = [];

  ngOnInit() {
    this.buildForm();
  }

  buildForm() {
    const formGroupFields = this.getFormControlsFields();
    this.dynamicFormGroup = new FormGroup(formGroupFields);
  }

  getFormControlsFields() {
    const formGroupFields = {};
    for (const field of Object.keys(this.model)) {
      formGroupFields[field] = new FormControl("");
      this.fields.push(field);
    }
    return formGroupFields;
  }
}

记住将html中的formGroup更新为dynamicFormGroup

接下来,创建新的组件dynamic-field,负责渲染该领域。添加两个Input()属性fieldformName

import {Component, Input} from "@angular/core";

@Component({
  selector: "app-field-input",
  templateUrl: "./dynamic-field.component.html",
  styleUrls: ["./dynamic-field.component.css"],
})
export class DynamicFieldComponent {
  @Input() field: {};
  @Input() formName: string;
}

添加带有输入和标签的HTML标记。

<form [formGroup]="formName">
    <label>{{field}}</label>
    <input type="text" [formControlName]="field"/>
</form>

打开app.component以将模型传递到动态形式。它负责处理模型,而dynamic-field呈现了输入。

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

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  model = {
    name: "",
    lastName: "",
    address: "",
    age: "",
  };
}

HTML通过定义传递属性模型。

<app-dynamic-form [model]="model"></app-dynamic-form>

完美,我们有一些单独的任务和责任。以下挑战显示了不同的控制类型。

按类型显示输入

动态形式呈现单一类型的输入。在现实世界中,我们需要更多类型的类型,例如dateselectinputradiocheckbox

有关控制类型的信息必须从模型到dynamic-field

使用以下属性typevaluelabel更改model。为了使它有点有趣,请更改type number的年龄,并创建一个新的birthDay of type date

model = {
    firstname: {
      type: "text",
      value: "",
      label: "FirstName",
    },
    lastname: {
      type: "text",
      value: "",
      label: "LastName",
    },
    address: {
      type: "text",
      value: "",
      label: "Address",
    },
    age: {
      type: "number",
      value: "",
      label: "age",
    },
    birthDay: {
      type: "date",
      value: "",
      label: "Birthday",
    },
  };
}

保存形式中显示的新字段birthDay

two

我们将对getFormsControlsFields方法进行小更改以处理元数据。

请创建一个新变量,fieldProps,以使用模型中的元数据存储该字段。使用值属性来分配formControl,并在字段数组中使用属性fieldName推动字段。

我们将在动态场组件中使用元数据属性

getFormControlsFields() {
    const formGroupFields = {};
    for (const field of Object.keys(this.model)) {
      const fieldProps = this.model[field];
      formGroupFields[field] = new FormControl(fieldProps.value);
this.fields.push({ ...fieldProps, fieldName: field });
    }
    return formGroupFields;
  }

最后,转到dynamic.component.html并使用这些属性field.label,更改formControlName以使用field.fieldName,然后用field.type绑定类型。

<form [formGroup]="formName">
    <label>{{field.label}}</label>
    <input [type]="field.type" [formControlName]="field.fieldName"/>
</form>

保存更改,并使用类型查看新控件。

three

添加选择,收音机和复选框

动态字段组件显示了输入,但是添加诸如selectradiocheckbox之类的控件使其变得有些复杂。我想将每个控件分为特定的控件。

为每个控制dynamic-input dynamic-radiodynamic-selectdynamic-checkbox创建组件。

ng g components/dynamic-field/dynamic-checkbox
ng g components/dynamic-field/dynamic-radio
ng g components/dynamic-field/dynamic-select
ng g components/dynamic-field/dynamic-input

每个组件都有两个共同点,带有元数据和FormGroup的字段都可以使用主形式。

让我们从输入和复选框开始:

输入

用元数据将field对象声明为输入属性。

export class DynamicInputComponent {
  @Input() field: {};
  @Input() formName: FormGroup;
}

在html标记中,使用标签和formControlNamefieldName使用元数据。

<form [formGroup]="formName">
    <label>{{field.label}}</label>
    <input [type]="field.type" [formControlName]="field.fieldName"/>
</form>

复选框

喜欢dynamic-input component,添加两个字段,带有字段元数据和formGroup

export class DynamicCheckboxsComponent {
  @Input() field: any;
  @Input() formName: FormGroup;
}

在HTML标记中,添加一个复选框。

<form [formGroup]="formName">
    <label>
        {{ field.label }}
        <input
                type="checkbox"
                [name]="field.fieldName"
                [formControlName]="field.fieldName"
                [value]="field.value"
        />
    </label>
</form>

出于个人原因,我想将复选框与输入分开;复选框有时具有特定的样式。

选择

属性是相同的,但是元数据会变得不同。选择有一个选项列表,因此我们需要使用 ngfor 指令迭代列表。

HTML标记看起来像这样:

<form [formGroup]="formName">

    <label>{{field.label}}:</label>
    <select [formControlName]="field.fieldName">
        <option *ngFor="let option of field.options" [value]="option.value">
            {{option.label}}
        </option>
    </select>
</form>

收音机

收音机很接近,类似于options列表的选择,但是在特定情况下,该名称必须相同以允许选择一个选项。我们添加了额外的label来显示选项label

<form [formGroup]="formName">
    <h3>{{field.label}}</h3>
    <label *ngFor="let option of field.options">

        <label ngFor="let option of field.options">
            <input type="radio"
                   [name]="field.fieldName"
                   [formControlName]="field.fieldName"
                   [value]="option.value"
            >
            {{option.label}}
        </label>
    </label>
</form>

好的,所有组件都准备就绪,但是有两个缺失点:显示组件并更新元数据。

显示动态组件和更新模型

我们有每种控制类型的组件,但是dynamic-field.component是它们之间的桥梁。

它按类型选择特定的组件。使用ngSwitch指令,我们确定与组件类型的控制匹配。

最终代码看起来像这样:

<ng-container [ngSwitch]="field.type">
    <app-dynamic-input *ngSwitchCase="'text'" [formName]="formName" [field]="field"></app-dynamic-input>
    <app-dynamic-select *ngSwitchCase="'select'" [formName]="formName" [field]="field"></app-dynamic-select>
    <app-dynamic-radio *ngSwitchCase="'radio'" [formName]="formName" [field]="field"></app-dynamic-radio>
    <app-dynamic-checkboxs *ngSwitchCase="'checkbox'" [formName]="formName" [field]="field"></app-dynamic-checkboxs>
</ng-container>

了解有关switchCase

的更多信息

接下来,我们为每种类型的元数据添加新字段:

typebussines:radio
SISSCRIPTYTYPE:select
新闻通讯:checkbox

类型radioselect必须具有带有{ label, value} fit组件期望的选项对象。

   typeBussines: {
      label: "Bussines Type",
      value: "premium",
      type: "radio",
      options: [
        {
          label: "Enterprise",
          value: "1500",
        },
        {
          label: "Home",
          value: "6",
        },
        {
          label: "Personal",
          value: "1",
        },
      ],
    },
    newsletterIn: {
      label: "Suscribe to newsletter",
      value: "email",
      type: "checkbox"
    },
    suscriptionType: {
      label: "Suscription Type",
      value: "premium",
      type: "select",
      options: [
        {
          label: "Pick one",
          value: "",
        },
        {
          label: "Premium",
          value: "premium",
        },
        {
          label: "Basic",
          value: "basic",
        },
      ],
    },

保存并重新加载。新组件可与结构一起使用,dynamic-field选择了特定的组件。

four

验证

我们需要一个完整的表格,并具有验证。我想简要介绍这篇文章,但是验证在表格中至关重要。

我的示例是添加所需验证器的基本示例,但请随时添加更多。

首先,我们必须使用新的元数据rules更改model,并带有required带有true值。

firstname: {
      type: "text",
      value: "",
      label: "FirstName",
      rules: {
        required: true,
      }
    },

验证器是表单控件的一部分。我们处理以新方法为addValidatorsformControl的验证器的规则,并在validators变量中存储的返回值以在formControl中分配。

      const validators = this.addValidator(fieldProps.rules);
      formGroupFields[field] = new FormControl(fieldProps.value, validators);

如果规则对象为空,请返回一个空数组

addValidator中,使用Object.keys并在rules对象中的每个属性上进行迭代。使用switch case进行数学值,然后返回Validator

在我们的情况下,所需的规则返回验证器。

  private addValidator(rules) {
    if (!rules) {
      return [];
    }

    const validators = Object.keys(rules).map((rule) => {
      switch (rule) {
        case "required":
          return Validators.required;
          //add more cases for the future.
      }
    });
    return validators;
  }

好的,我们已经使用验证器配置了formControl,但是如果控件无效,我们需要显示标签。创建一个新组件dynamic-error,具有两个输入属性,formGroupfieldName

import { Component, Input } from "@angular/core";
import { FormGroup } from "@angular/forms";

@Component({
  selector: "app-dynamic-error",
  templateUrl: "./dynamic-error.component.html",
  styleUrls: ["./dynamic-error.component.css"],
})
export class DynamicErrorComponent {
  @Input() formName: FormGroup;
  @Input() fieldName: string;
}

我们使用html中的表单参考来找到按名称的控件。如果是invaliddirtytouched,请显示消息。

<div *ngIf="formName.controls[fieldName].invalid && (formName.controls[fieldName].dirty || formName.controls[fieldName].touched)"
     class="alert">
    <div *ngIf="formName.controls[fieldName].errors.required">
        * {{fieldName}}
    </div>
</div>

最后,在dynamic-form组件中添加dynamic-error组件并通过fieldNameformGroup

<form [formGroup]="dynamicFormGroup">
    <div *ngFor="let field of fields">
        <app-field-input [formName]="dynamicFormGroup" [field]="field"></app-field-input>
        <app-dynamic-error [formName]="dynamicFormGroup" [fieldName]="field.fieldName"></app-dynamic-error>
    </div>
</form>

阅读有关validators in Angular

的更多信息

finl

是的!验证者使用我们的动态形式。

如果您到达此部分,则具有稳定的动态形式。我尝试使这篇文章简短,但是我听到了其他用户的反馈,例如 @Juan Berzosa Tejero和...。

重构时间

FormGroup的传播

@Juan Berzosa Tejero花点时间回顾了这篇文章,他向我询问了使用formName@Input()FormGroup传播,然后开始发出噪音。幸运的是,我在角文档中找到了指令FormGroupDirective。它可以帮助我们将现有的FormGroupFormRecord绑定到DOM元素。

我决定使用和重构代码,我们将从dynamic-error.component开始以简化,但是所有子组件的步骤都相似。

formName中卸下@Input()装饰器,然后将FormGroupDirective注入组件构造函数。

添加ngOnInit生命周期以用FormGroupDirective.control设置formName以将FormGroup绑定到它。

最终代码看起来像这样:

import { Component, Input, OnInit } from "@angular/core";
import { FormGroup, FormGroupDirective } from "@angular/forms";

@Component({
  selector: "app-dynamic-error",
  templateUrl: "./dynamic-error.component.html",
  styleUrls: ["./dynamic-error.component.css"],
})
export class DynamicErrorComponent implements OnInit {
  formName: FormGroup;
  @Input() fieldName: string;

  constructor(private formgroupDirective: FormGroupDirective) {}

  ngOnInit() {
    this.formName = this.formgroupDirective.control;
  }
}

dynamic-form不再需要通过formGroupName。它只需要元数据。代码看起来像这样:

<form [formGroup]="dynamicFormGroup">
    <div *ngFor="let field of fields">
        <app-field-input [field]="field"></app-field-input>
        <app-dynamic-error [fieldName]="field.fieldName"></app-dynamic-error>
    </div>
</form>

如果您对所有子组件复制相同,则dynamic-field不再需要设置formName

<ng-container [ngSwitch]="field.type">
    <app-dynamic-input *ngSwitchCase="'text'" [field]="field"></app-dynamic-input>
    <app-dynamic-input *ngSwitchCase="'number'" [field]="field"></app-dynamic-input>
    <app-dynamic-select *ngSwitchCase="'select'" [field]="field"></app-dynamic-select>
    <app-dynamic-radio *ngSwitchCase="'radio'" [field]="field"></app-dynamic-radio>
    <app-dynamic-checkboxs *ngSwitchCase="'checkbox'" [field]="field"></app-dynamic-checkboxs>
</ng-container>

完成,我们做了重构!请随时阅读有关FormGroup Directive的更多信息。

删除Ngswitch

昨天, @Maxime Lyakhov,留下有关ngswich的消息。他对HTML的Ngswitch是正确的。很难维护。

我的第一个想法是使用ViewChildViewContainerRef动态加载特定组件,并使用setInput()方法设置输入变量。

注意:我将项目更新为Angular 14,因为API to load dynamic components更容易。

首先,在ng-container中添加模板变量,以使用ViewChild进行引用。

<ng-container #dynamicInputContainer>

</ng-container>

接下来,声明指向DynamicInput容器的ViewChild,它是我们动态组件的占位符。

@ViewChild('dynamicInputContainer', { read: ViewContainerRef}) dynamicInputContainer!: ViewContainerRef;

添加一个带有键和组件的所有受支持组件的新数组。

supportedDynamicComponents = [
    {
      name: 'text',
      component: DynamicInputComponent
    },
    {
      name: 'number',
      component: DynamicInputComponent
    },
    {
      name: 'select',
      component: DynamicSelectComponent
    },
    {
      name: 'radio',
      component: DynamicRadioComponent
    },
    {
      name: 'date',
      component: DynamicInputComponent
    },
    {
      name: 'checkbox',
      component: DynamicCheckboxsComponent
    }
  ]

注意:服务可以提供受支持的组件或外部变量的列表,但我尝试将文章简短。

创建getComponentByType方法以在SuppertedDynamicComponents中找到组件,如果不存在返回DynamicInputComponent。

getComponentByType(type: string): any {
    const componentDynamic = this.supportedDynamicComponents.find(c => c.name === type);
    return componentDynamic.component || DynamicInputComponent;
  }

接下来,一种新方法registerDynamicField()。它负责从getComponentType()创建实例并设置组件所需的输入字段。

我们执行三个步骤:

  1. 使用字段属性键入组件并存储在componentInstance变量中。
  2. 使用CreateComponent通过实例并获取动态组件。
  3. 使用setInput()方法将字段传递到输入field
private registerDynamicField() {

    const componentInstance = this.getComponentByType(this.field.type)
    const dynamicComponent = this.dynamicInputContainer.createComponent(componentInstance)
    dynamicComponent.setInput('field', this.field);
    this.cd.detectChanges();
  }

由于输入属性字段更改,我们需要触发更改检测以保持组件同步。

_LEALN有关ChangeDetection_

的更多信息

观看式的频场仅在AfterViewInit LifeCycle上可用,实现界面并调用方法registerDynamicField

  ngAfterViewInit(): void {
    this.registerDynamicField();
  }

保存更改,一切都按预期继续工作,而Ngswitch消失了。

Learn more about viewchild

## recap

我们学会了如何从结构中添加动态字段,并生成诸如selectcheckboxradioinputs的输入几次。该模型可以是后端的API响应。

如果要添加一个新字段,请将其添加到API响应中,您可以随意在动态场组件中添加更多类型。

我们可以通过使用每种组件类型的接口来改进代码,例如dropdowncheckbox或表单本身。另外,创建辅助功能以创建大多数下拉式的样板代码。

如果您愿意,请随时分享!