前回は、
- ES6とTypescriptの基礎
- Typescriptの型定義について
- Angularでのデータ管理の基礎2
- サービスクラスの作成
を学びました。
本記事では、
- HTMLのフォーム要素とTypescriptの連携
- [(ngModel)] を利用した双方向バインディング
- クリックイベント
- フォームの送信
について学んでいきます。
この記事のソースコード
https://github.com/seteen/AngularGuides/tree/入門その06
HTMLのフォーム要素とTypescriptの連携
HTMLのフォームの要素である、 <input>
, <select>
などの要素は、
画面での入力が内部のTypescriptのコードに反映してほしかったり、逆にTypescriptのコードで計算した値を入れたいような場合があります。
そのような場合に Angluar では、 [(ngModel)]
という要素を使います。
[(ngModel)]
は、双方向バインディングというもので、TypescriptからHTMLへのバインドと、HTMLからTypescriptへのバインドの双方向でデータが同期される仕組みです。
ちなみに、今までに何度か出てきた [property]
のように []
で囲むのは、 Typescript から HTMLへのバインド
(click)
のように ()
で囲むのが HTML から Typescript へのバインドになります。 [(ngModel)]
はどちらからも囲まれているので双方向ということですね。
TIPS: Angularのバインディング
詳しくは、こちらを参照ください。
https://angular.io/guide/architecture-components#data-binding
まずは、この [(ngModel)]
を解説していきます。
編集用のページの作成
最初に、フォームを利用するページを新しく作成します。
AngularCLI
を利用してコマンドで作りましょう。
# ng g component product/product-edit
CREATE src/app/product/product-edit/product-edit.component.scss (0 bytes)
CREATE src/app/product/product-edit/product-edit.component.html (31 bytes)
CREATE src/app/product/product-edit/product-edit.component.spec.ts (664 bytes)
CREATE src/app/product/product-edit/product-edit.component.ts (293 bytes)
UPDATE src/app/app.module.ts (738 bytes)
コンポーネントを作成したので、ルートに追加します。
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from '@angular/core';
import { ProductListComponent } from './product/product-list/product-list.component';
import { ProductDetailComponent } from './product/product-detail/product-detail.component';
import { ProductEditComponent } from './product/product-edit/product-edit.component';
const routes: Routes = [
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductDetailComponent },
{ path: 'products/:id/edit', component: ProductEditComponent },
{ path: '', redirectTo: '/products', pathMatch: 'prefix' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule {}
ng serve
で起動して、 http://localhost:4200/products/test/edit
にアクセスしてみましょう。
ちゃんと画面が追加されていますね。
AngularCLI
を使ってコンポーネントを作成したので、実は気づいていないかもしれませんが、実は app.module.ts
が自動で変更されています。
CLIを使わずに自分で追加する場合は、気をつけましょう。
@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductDetailComponent,
ProductEditComponent // <= 勝手に追加されている
],
imports: [
BrowserModule,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ここまでのコミット
フォームを使って編集画面を作る
コンポーネントが追加できたので、スタイルを当てていきます。
まずは、HTMLとCSSを修正して編集画面にしていきます。
<div class="container">
<div class="title">商品編集</div>
<form class="edit-form">
<div class="edit-line">
<label>ID</label>
<span>1</span>
</div>
<div class="edit-line">
<label>名前</label>
<input type="text">
</div>
<div class="edit-line">
<label>価格</label>
<input type="number">
</div>
<div class="edit-line">
<label>説明</label>
<input type="text">
</div>
</form>
<div class="footer">
<div class="button black" routerLink="/products">保存</div>
</div>
</div>
.container {
margin: auto;
padding: 32px 0;
width: 800px;
.title {
padding: 8px 0;
text-align: center;
width: 100%;
font-weight: 600;
font-size: 18px;
}
.edit-form {
padding: 16px 48px;
border: 1px solid #D9DBDE;
border-radius: 4px;
background-color: #FFFFFF;
.edit-line {
display: flex;
align-items: center;
padding: 16px 0;
width: 100%;
label {
width: 15%;
font-size: 16px;
font-weight: 600;
}
input {
border: 1px solid #BDBDBD;
border-radius: 4px;
padding: 0 8px;
width: 560px;
height: 40px;
font-size: 14px;
}
}
}
.footer {
display: flex;
justify-content: center;
padding: 24px 0;
}
}
この段階で再度起動して確認してみます。
ng serve
で起動して、 http://localhost:4200/products/test/edit
にアクセスしてみましょう。
ちゃんとスタイルがあたり、編集画面っぽくなりました。
ここまでのコミット
[(ngModel)]
を使ってデータを反映する
編集画面では、データを API で取得し、そのデータを編集したいという場合が多いです。
この場合、画面を始めて開いたときにAPIで取得したデータが入っていることが望ましいです。
また、このとき、ブラウザ上でユーザが編集したデータをそのままAPIで登録するという場合も多いです。
このような場合は Typescript -> HTML
および HTML -> Typescript
の双方向のバインドが適しています。
実際に [(ngModel)]
を利用してみましょう。
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
@Component({
selector: 'app-product-edit',
templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
product: Product;
constructor(
private productService: ProductService,
) {}
ngOnInit() {
this.productService.get(2).subscribe((product: Product) => {
this.product = product;
});
}
}
<div class="container">
<div class="title">商品編集</div>
<form class="edit-form">
<div class="edit-line">
<label>ID</label>
<span>{{ product.id }}</span>
</div>
<div class="edit-line">
<label>名前</label>
<input type="text" name="name" [(ngModel)]="product.name">
</div>
<div class="edit-line">
<label>価格</label>
<input type="number" name="price" [(ngModel)]="product.price">
</div>
<div class="edit-line">
<label>説明</label>
<input type="text" name="description" [(ngModel)]="product.description">
</div>
</form>
<div class="footer">
<div class="button black" routerLink="/products">保存</div>
</div>
</div>
注意
Form
内で利用する [(ngModel)]
は、実は name
属性を指定しないとエラーになります。
そのため、各 input 要素に name
属性を追加しています。
ここで、もう再度起動して動作確認してみましょう。
残念ながら、真っ白な画面になっています。
Chrome の デベロッパーツールを起動してエラーを確認します。
compiler.js:215 Uncaught Error: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.
これは、 [(ngModel)]
がAngularに存在を認識されていないという意味です。 謎の要素があるけど、これ何?
ということですね。
実はAngularでは、モジュール (今回のプロジェクトでは、 app.module.ts
) に登録していない機能はAngularのプロジェクトに認識されないので利用できません。
これは、本番用にビルドされるJSに不要なデータを入れず小さくするためのものだと思います。
このエラーは今後頻繁に遭遇することになると思いますので対応を覚えましょう。
まず、今回は自分で追加した要素ではなくAngularの標準機能を利用しています。
そのため、公式サイトの https://angular.io で ngModel で検索してみます。
検索して、 ngModel のページに行きます。
このページは、 ngModel
の解説をしてくれているページです。
このページで、 module で検索すると、下記が引っかかります。
NgModule: FormsModule
と書いてあります。
これはつまり、 ngModel
は FormsModule
に所属しているという意味です。
ここまでの手順は、どのクラスがどのモジュールに所属しているか覚えない限り同じことをやると思いますので、覚えておきましょう。
app.module.ts
に FormsModule
を追加します。
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 } from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductDetailComponent,
ProductEditComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule, // <- ここを追加
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
この状態で再度動作確認してみましょう。
input
に値が反映されていますね。
これまでのコミット
クリックイベントで保存処理を行う
Webサイトの開発において、頻繁に利用するクリックイベント、Angularにおけるクリックイベントの取得方法を解説していきます。
クリックなどのHTML要素に何かしらのアクションを起こしたときのイベントは、 HTML -> Typescript
の向きのバインドになります。
そのため、 ()
で囲うことで実現されます。 ( 反対に []
で囲うと Typescript -> HTML
のバインドでしたね)
クリックやスクロールなどのJavascriptが標準で準備しているイベントは、Angularが最初から用意してくれています。
クリックイベントは、 (click)
で定義されています。
TIPS: 使えるイベントを調べる
WebStorm では、実は下記のようにHTMLタグに(
を書くと補完で使えるイベントを出してくれます。
便利です。
では、編集ページの保存ボタンをクリックすることで保存処理 ( saveProduct()
) が実行されるとして実装を進めていきます。
<div class="container">
<div class="title">商品編集</div>
<form class="edit-form">
<div class="edit-line">
<label>ID</label>
<span>{{ product.id }}</span>
</div>
<div class="edit-line">
<label>名前</label>
<input type="text" name="name" [(ngModel)]="product.name">
</div>
<div class="edit-line">
<label>価格</label>
<input type="number" name="price" [(ngModel)]="product.price">
</div>
<div class="edit-line">
<label>説明</label>
<input type="text" name="description" [(ngModel)]="product.description">
</div>
</form>
<div class="footer">
<div class="button black" (click)="saveProduct()">保存</div>
</div>
</div>
import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/models/product';
import { ProductService } from '../../shared/services/product.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-product-edit',
templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.scss']
})
export class ProductEditComponent implements OnInit {
product: Product;
constructor(
private router: Router,
private productService: ProductService,
) {}
ngOnInit() {
this.productService.get(2).subscribe((product: Product) => {
this.product = product;
});
}
saveProduct(): void {
console.log(this.product);
this.router.navigate(['/products']);
}
}
保存ボタンをクリックすると、 saveProduct()
の処理が走り、商品一覧ページに遷移するようにしました。
起動して動作確認してみましょう。
解説
クリックイベント
まず、 Typescript に saveProduct()
というメソッドを用意しました。
(click)
要素は、 同じコンポーネントのTypescript に記載されているメソッドを入力としてとります。
(click)="saveProduct()"
と書くことで、要素がクリックされると saveProduct()
を呼び出すという意味になります。
saveProduct()
の 内容
saveProduct()
メソッドは、 console.log
で Product の情報を出力し、 /products
に遷移するというものです。
実は今回、 router: Router
という部分を constructor
に追加しています。
Router
というのは、Angular内でのルートの移動や現在のルートの情報を持っているクラスです。
navigate
メソッドは、 HTML上で記載していた routerLink
と同じような動きをし、
this.router.navigate(['/products']);
と書くことで http://localhost:4200/products
に遷移させることができます。
本来はこのメソッドで編集用のAPIを呼び出すところですが、今回は双方向バインドによりメモリ上の保存はすでに終わっているので、保存処理を実行していません。
TIPS: Router
Routerで他に何ができるのか、詳しく知りたい方は公式ページで確認してみましょう。
https://angular.io/api/router/Router
TIPS: 双方向バインディングの罠
今回の例でも見た通り、双方向バインドでは、HTMLで入力したものがそのままTypescript上の変数の値に反映されます。
その場合、例えばキャンセルボタンのようなものを用意している場合、今回のケースでは以前のProductの状態を保存していないため、もとに戻すことができません。
このように双方向バインディングを用いると、予期しない挙動をすることもあるので、注意しましょう。
そうならないようにするために、フォームで利用するデータは新しく用意した要素を用いる、そもそも双方向バインディングを使わないなどの工夫とすると良いと思います。
ここまでのコミット
クリックイベントおまけ。イベント情報の取得
クリックイベントなどで、イベントの情報を利用したいときがあります。
クリックだと良い例が思いつかなかったのですが、例えばスクロールイベントだと、スクロール位置が取得したい、などです。
このような場合は、 (click)="saveProduct($event)
のように記述します。
Typescript 側の定義を saveProduct(event: MouseEvent): void
のように変更すれば、 この引数の event
に値が入ります。
型がわからない、という場合は、いったん any
で定義して実際に動かしてみて 型を調べると良いです。
ngSubmit を利用したフォームの制御
クリックイベントについて学んできましたが、実はこの方法では Form
を利用していません。
この場合、例えばInput要素でEnterを押したら自動でフォームを送信する、などの恩恵は受けられません。
ここからは、 Form
の submit
イベントをAngularで補足し、 Typescript
のメソッドを呼び出すように改造していきます。
そこで登場するのが、 ngSubmit
です。
これは、 form
要素が持てるディレクティブなので、 form
要素に追加してみます。
<div class="container">
<div class="title">商品編集</div>
<form (ngSubmit)="saveProduct()">
<div class="edit-form">
<div class="edit-line">
<label>ID</label>
<span>{{ product.id }}</span>
</div>
<div class="edit-line">
<label>名前</label>
<input type="text" name="name" [(ngModel)]="product.name">
</div>
<div class="edit-line">
<label>価格</label>
<input type="number" name="price" [(ngModel)]="product.price">
</div>
<div class="edit-line">
<label>説明</label>
<input type="text" name="description" [(ngModel)]="product.description">
</div>
</div>
<div class="footer">
<button class="button black">保存</button>
</div>
</form>
</div>
今回は、HTMLだけ変更しています。
保存ボタンを div
から button
要素にし、 form
タグの中に移動しました。 (click イベントを消しました。 button
に変更したので、クリックすると自動的にformを送信しようとします)
また、その影響でスタイルが崩れないように form
に div
要素を追加しています。
動作確認をしてみましょう。
input
中で Enter を押すだけでフォームが反応して、 saveProduct()
が動き、 http://localhost:4200/products
へ遷移していることがわかります。
TIPS: (submit) vs (ngSubmit)
実は、Angularには (submit) というものも用意されています。
(ngSubmit) は、メソッド内部で throw するとフォームの送信を停止してくれる、などの違いがあるので、 (ngSubmit) を利用すると良いです。
参考↓
https://stackoverflow.com/questions/41448038/difference-between-angular-submit-and-ngsubmit-events?rq=1
ここまでのコミット
まとめ
今回は、Angularにおけるフォームの使い方を通して [(ngModel)]
の双方向バインディング、クリックイベント、 ngSubmit
の使い方を学びました。
次回 は、これまでの学習内容の実践編として、これまで学んだ技術を使って各ページを改善したり、 URL
パラメータを取得してその内容に応じてページの内容を変更する方法を学んでいきます。
Angular入門 未経験から1ヶ月でサービス作れるようにする その6.5 URLパラメータの利用とここまで学んだことの実践
入門記事一覧
「Angular入門 未経験から1ヶ月でサービス作れるようにする」は、記事数が多いため、まとめ記事 を作っています。
https://qiita.com/seteen/items/43908e33e08a39612a07