七.利用 Angular 的响应式表单,让零基础用户一个月内能够制作服务

上一次,通过实践运用之前所学的知识,我加深了对Angular的了解。

在本文中,我们将学习如何使用响应式表单来编写不使用 [ngModel] 的表单。

    • ReactiveFormの基礎

 

    • [(ngModel)] をFormGroup, FormControlで書き換える

 

    Formの送信による保存処理

我会继续做下去。

这篇文章的源代码

https://github.com/seteen/AngularGuides/tree/Introduction07 的原文需要翻译成中文

Angular中表单元素的基础

目前的 product-edit.component.ts 使用 [(ngModel)] 将表单的每个元素与产品的各个要素对应起来。

在Angular中,为了处理与表单相关联的操作,提供了专门的类进行处理。

关于每一个,我将逐步解释。

クラス役割FormControl<input> などの入力要素一つに対応するクラスFormGroup<form> 要素に対応するクラスFormBuilderFormGroup, FormControl を作成するクラス

与使用 [(ngModel)] 的示例相对应

考虑到当前的product-edit.component.ts,对应关系如下所示。

クラス役割product-edit.componentFormControl<input> などの入力要素一つに対応するクラスproductFormGroup<form> 要素に対応するクラスproduct.nameFormBuilderFormGroup, FormControl を作成するクラスなし

将 [(ngModel)] 替换为 FormGroup、FormControl 写法

首先,我们会从Typescript开始进行重写。

import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
  productForm = this.fb.group({ // <= 変更
    id: [''],
    name: [''],
    price: [''],
    description: [''],
  });

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private fb: FormBuilder, // <= 追加
    private productService: ProductService,
  ) {}

  ngOnInit() {
    this.route.params.subscribe((params: Params) => {
      this.productService.get(params['id']).subscribe((product: Product) => {
        this.productForm.setValue({ // <= 変更
          id: product.id,
          name: product.name,
          price: product.price,
          description: product.description,
        });
      });
    });
  }

  saveProduct(): void {
    this.router.navigate(['/products']);
  }
}

讲解

构造函数部分

在代码中添加了FormBuilder。

变量定义部分

我删除了product变量,并添加了productForm变量。

产品的形式是FormGroup。使用this.fb.group()函数创建一个FormGroup。

这个方法的参数是作为表单元素值的键和初始值(数组的形式是为了能够在第二个以后的位置放入验证器,这将在后面解释)。

换句话说,这里我们正在创建一个具有与 product 相同的元素(id、name、price、description)的 FormGroup。

另外,每个名字:[”] 是一个表单控件。

我会在下面写下不使用FormBuilder就可以实现类似功能的代码,因为使用FormBuilder不太容易理解哪些元素属于哪个类。

productForm = new FormGroup({
  id: new FormControl(''),
  name: new FormControl(''),
  price: new FormControl(''),
  description: new FormControl(''),
});

ngOnInit 部分

在从产品服务中取出产品后,将当前产品的值放入产品表单中。

使用 HTML 中的 FormGroup、FormControl

<div class="container">
  <div class="title">商品編集</div>
  <form (ngSubmit)="saveProduct()" [formGroup]="productForm">
    <div class="edit-form">
      <div class="edit-line">
        <label>ID</label>
        <span>{{ productForm.controls.id.value }}</span>
      </div>
      <div class="edit-line">
        <label>名前</label>
        <input type="text" formControlName="name" required>
      </div>
      <div class="edit-line">
        <label>価格</label>
        <input type="number" formControlName="price">
      </div>
      <div class="edit-line">
        <label>説明</label>
        <input type="text" formControlName="description">
      </div>
    </div>
    <div class="footer">
      <span class="button white" [routerLink]="['/products', productForm.controls.id.value]">キャンセル</span>
      <button class="button black">保存</button>
    </div>
  </form>
</div>

通过在

元素上使用 [formGroup] 属性来关联 productForm 表单。

另外,我們從每個 元素中刪除了 [(ngModel)],並且添加了 formControlName,以將其與 productForm 的每個元素關聯起來。

由于ID是不允许编辑的,所以我们通过productForm.control.id.value来提取并直接显示。

新增一个新模块

其实,在这里启动也会出现错误。

compiler.js:215 Uncaught Error: Template parse errors:
Can't bind to 'formGroup' since it isn't a known property of 'form'. ("<div class="container">
  <div class="title">商品編集</div>
  <form (ngSubmit)="saveProduct()" [ERROR ->][formGroup]="productForm">
    <div class="edit-form">
      <div class="edit-line">

這表示模組不足。我們將按照第六步介紹的方法進行尋找。

我尝试访问 https//angular.io 网站寻找 FormGroup。

001.png

在这里使用模块进行搜索。……找不到!

实际上,Angular的官方文档在这方面相当糟糕,很难理解。

其实,’Can’t bind to ‘xxx’ since it isn’t a known property of ‘yyyy” 这个错误实际上是指缺少指令(类似于HTML中的属性),所以出现了该错误。

在上述的图像中进行的调查实际上是一个类而不是指令。

当你在 https://angular.io 上搜索 FormGroup,会出现如下的页面。

002.png

在这里,被标记为 C 的是类,被标记为 D 的是指令。

在 FormGroup 下面有一个称为 FormGroupDirective 的东西。

其实,我们必须调查这个。

003.png

当我点击FormGroupDirective界面时,会跳转到上述的界面。

在这里

@Directive({
    selector: '[formGroup]',
    providers: [formDirectiveProvider],
    host: { '(submit)': 'onSubmit($event)', '(reset)': 'onReset()' },
    exportAs: 'ngForm'
})

写着「と」と记载。

这个指令的选择器是 [formGroup]。也就是说,它的名字是 Directive,让我有些困惑,但这确实是我要查找的内容。

我来在这里搜索一下模块。

004.png

是的,确实发生了。

此 [formGroup] 是屬於 ReactiveFormModule 的。

将其添加到 app.module.ts 文件中。

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

import { AppComponent } from './app.component';
import { ProductListComponent } from './product/product-list/product-list.component';
import { ProductDetailComponent } from './product/product-detail/product-detail.component';
import { AppRoutingModule } from './app-routing.module';
import { ProductEditComponent } from './product/product-edit/product-edit.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    ProductListComponent,
    ProductDetailComponent,
    ProductEditComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    ReactiveFormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

让我们在这种情况下进行操作确认。

我将使用 ng serve 命令来启动,并尝试访问 http://localhost:4200/products/1/edit。

005.png

很好,值已经被正确地填充了。 FormGroup、FormControl已经成功地与

元素进行了关联。

提示:深奥的Form元素
在本次中,我介绍了三个类,但是Angular还提供了其他与Form相关的类。
请详细了解请访问Angular的官方网站。
https://angular.io/guide/reactive-forms

直到这个提交为止。

通过表单提交进行保存处理

在使用 FormGroup 和 FormControl 的情况下,我逐步将 [(ngModel)] 进行替换。

在当前的这个应用程序中,所有的状态都是在内存中处理的。当使用[(ngModel)]时,由于双向绑定,如果不采取任何操作,它将被直接保存。

由于这次不再使用 [(ngModel)],所以没有更改 ProductService 持有的 products 的处理过程,所以更改不会被保存。

我們將在 ProductService 中添加一個 update() 方法,以便對變更進行保存。

import { Injectable } from '@angular/core';
import { Product } from '../models/product';
import { Observable, of } from 'rxjs/index';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  products = [
    new Product(1, 'Angular入門書「天地創造の章」', 3800, '神は云った。「Angularあれ」。するとAngularが出来た。'),
    new Product(2, 'Angularを覚えたら、年収も上がって、女の子にももてて、人生が変わりました!', 410, '年収300万のSEが、Angularと出会う。それは、小さな会社の社畜が始めた、最初の抵抗だった。'),
    new Product(3, '異世界転生から始めるAngular生活(1)', 680,
      'スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?'),
  ];

  constructor() { }

  list(): Observable<Product[]> {
    return of(this.products);
  }

  get(id: number): Observable<Product> {
    return of(this.products[id - 1]);
  }

  update(product: Product): void { // <= 追加
    const index = this.products.findIndex((prd: Product) => prd.id === product.id);
    this.products[index] = product;
  }
}
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { FormBuilder } from '@angular/forms';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
  productForm = this.fb.group({
    id: [''],
    name: [''],
    price: [''],
    description: [''],
  });

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private fb: FormBuilder,
    private productService: ProductService,
  ) {}

  ngOnInit() {
    this.route.params.subscribe((params: Params) => {
      this.productService.get(params['id']).subscribe((product: Product) => {
        this.productForm.setValue({
          id: product.id,
          name: product.name,
          price: product.price,
          description: product.description,
        });
      });
    });
  }

  saveProduct(): void {
    const { id, name, price, description } = this.productForm.getRawValue(); // <= 追加
    this.productService.update(new Product(id, name, price, description)); // <= 追加
    this.router.navigate(['/products', this.productForm.controls.id.value]); // <= 変更
  }
}

说明

有关产品服务的内容

我为 ProductService 添加了 update() 方法。

update(product: Product): void {
  const index = this.products.findIndex((prd: Product) => prd.id === product.id);
  this.products[index] = product;
}

处理的步骤是,通过update提供的product查找具有相同id的索引,在products中将索引位置的元素与输入的product进行交换。

提示:findIndex() 方法
这是Javascript中Array对象的一个方法。它会对数组的每个元素执行指定的函数,并返回第一个返回 true 的元素的索引值。
参考链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex

有关ProductEditComponent

我改变了saveProduct()方法。

saveProduct(): void {
  const { id, name, price, description } = this.productForm.getRawValue(); // <= 追加
  this.productService.update(new Product(id, name, price, description)); // <= 追加
  this.router.navigate(['/products', this.productForm.controls.id.value]); // <= 変更
}

首先是第一行

const { id, name, price, description } = this.productForm.getRawValue();

我将对此进行解释。

这个.productForm.getRawValue() 方法会将 FormGroup 内的 FormControl 值以 key: value 的形式转换成对象并返回。

如果本次只对 id 为3的产品进行执行而不进行任何编辑,将返回下列结果。

{id: 3, name: "異世界転生から始めるAngular生活(1)", price: 680, description: "スパゲッティの沼でデスマーチ真っ最中の田中。過酷な日々からの現実逃避か彼は、異世界に放り出され、そこでAngularの入門書を拾う。現実逃避でさえ、プログラミングをするしかない彼に待ち受けるのは!?"}

下一个是,

const { xxx } = yyy 是将 yyy 对象的 xxx 元素存入变量 xxx 中的操作。

因此,这意味着将 FormGroup 的每个值分别赋给变量 id、name、price 和 description。

在下一行中,调用了 ProductService 中新创建的 update() 方法。

尽管最后一行并不需要特别更改,但在保存后转到列表视图,因此我进行了修正以转到详细视图。

让我们进行操作确认。

006.gif

现在保存的内容能够正确地反映出来了。

迄今的提交

总结

這次我們學習了關於不使用 [(ngModel)] 的 Reactive forms。雖然相比於 [(ngModel)] 來說稍微冗長一些,但是在下一個版本中加入驗證部分後,這種方式也變得相當方便使用。

下次我们将解释一下Angular的验证方法。

8. 验证-使Angular初学者能够在一个月内创建服务

入门指南列表

「Angular入門 未経験から1ヶ月でサービスを作成できるようにする」という記事は、記事数が多いため、まとめ記事を作成しています。
https://qiita.com/seteen/items/43908e33e08a39612a07

广告
将在 10 秒后关闭
bannerAds