データサービスとそのサブスクライブ
Angular に限らないのですが、様々なシーンでよく行われる実装として、 「データサービス」的な機能を作り、それを各モジュールないしコンポーネントでディペンデンシ・インジェクションで取り込み、 そのデータサービスへのデータの追加削除を、必要な場所でサブスクライブすることで変更を受け取る、というパターンがあります。
「データサービス」と呼んだり、「データストア」と呼んだり、呼び方は言語やライブラリなどの習慣・カルチャによってまちまちですけどね。
ここでは簡易的な「データサービス」の実装方法を紹介します。
実装の概要
ここでは次のスクリーンショット (動作例) のように、上部に入力フォームがあり、下部にデータのリスト部がある画面です。
入力フォームの箇所 (AddDataComponent という名前にします) には入力フィールドとボタンがあり、 ボタンをクリックした時にデータを入力します。
入力された文字は DataService という名前のサービス内の配列に保持します。
表示部のコンポーネント (ShowDataComponent という名前にします) はデータサービスの変更を購読 (サブスクライブ) しており、 変更があった時に、それを検出して画面に表示しています。
また、これはおまけですが、見栄えを整えるために Bootstrap を利用しています。
プロジェクトの作成と必要なパッケージのインストール
それでは Angular プロジェクトを作成します。名前はなんでも構いません。ここでは適当に test1 とします。
$ ng new test1 $ cd test1
必要なパッケージは、今回は Bootstrap を使う分の手間だけです。 見栄えを整えるだけで本題には関わらないので、ここには書きません。 「Bootstrap (ng-bootstrap) の利用方法」を参考にしてください。
コンポーネントの生成と登録
入力フォーム用のコンポーネント AddDataComponent と、 データ表示用のコンポーネント ShowDataComponent を作成します。
$ ng generate component add-data --skipTests $ ng generate component show-data --skipTests
--skipTests はテスト用のファイルを生成しないためのオプションです。付けなくても構いません。
app.module.ts の declarations にコンポーネントを追加します。
import { AddDataComponent } from './add-data/add-data.component';
import { ShowDataComponent } from './show-data/show-data.component';
@NgModule({
declarations: [
AppComponent,
AddDataComponent,
ShowDataComponent
],
サービスの作成と登録
データサービス DataService を作成します。
$ ng generate service data --skipTests
インジェクト可能にするために、app.module.ts の providers に DataService を登録します。
app.module.ts は次のようになります。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AddDataComponent } from './add-data/add-data.component';
import { ShowDataComponent } from './show-data/show-data.component';
import { DataService } from './data.service';
@NgModule({
declarations: [
AppComponent,
AddDataComponent,
ShowDataComponent
],
imports: [
BrowserModule,
NgbModule,
AppRoutingModule],
providers: [
DataService
],
bootstrap: [
AppComponent
]
})
export class AppModule {}
データサービスの実装
データサービス DataService のポイントは、 RxJS の Subject を利用して、データ変更イベントを実装しているところです。
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
data: string[] = [];
dataChanged = new Subject<string[]>();
constructor() {}
add(s: string) {
if (!s) {
return;
}
this.data.push(s);
this.data.sort();
this.dataChanged.next(this.data);
}
}
データ入力を受け付ける add メソッドにて、受け取った文字列を文字列の配列 data に追加、ソートしたのち、 Subject である dataChanged の next メソッドを呼びます。
このとき文字列の配列 data を渡しています。これによって、このイベントをサブスクライブしている箇所で変更済みデータを受け取ります。
入力フォームの実装
文字の入力フォームは次のようにしました。
app/add-data/add-data.component.html
<div class="form1">
<div class="form-group">
<input
type="text"
class="form-control"
[value]="text1"
(input)="text1 = $event.target.value" />
</div>
<button
class="btn btn-primary"
(click)="addClick()">OK</button>
</div>
input フィールドとボタンをひとつずつ配置しているだけです。
input フィールドの value 属性に二方向バインディングで、 以下の add-data.component.ts のプロパティ text1 とバインドしています。
app/add-data/add-data.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-add-data',
templateUrl: './add-data.component.html',
styleUrls: ['./add-data.component.css']
})
export class AddDataComponent {
text1 = '';
constructor(private dataService: DataService) {}
addClick() {
this.dataService.add(this.text1);
this.text1 = '';
}
}
ここではデータサービス DataService を DI で取り込んでいて、 ボタンを押した時のハンドラでデータサービスの add メソッドに入力フィールドに 入力された文字を渡しています。
ちなみに、フォームの CSS は次のようにしています。
app/add-data/add-data.component.css
.form1 {
width: 400px;
margin: 10px;
}
input[type="text"] {
font-weight: bold;
}
さて、上の入力フォームで入力された文字は、データサービスの add メソッドを通して、 データサービス内の文字列配列に格納されています。
それを受け取り、表示するにはどうすればよいでしょうか。
データ表示用コンポーネントの実装 その 1
〜 Subject をサブスクライブする
データサービスからデータを受け取り、表示するコンポーネントを実装するひとつめの方法は、 dataChanged をサブスクライブすることです。
app/add-data/show-data.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from '../data.service';
import { Subscription, Observable } from 'rxjs';
@Component({
selector: 'app-show-data',
templateUrl: './show-data.component.html',
styleUrls: ['./show-data.component.css']
})
export class ShowDataComponent implements OnInit, OnDestroy {
dataSubs = new Subscription();
data: string[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataSubs = this.dataService.dataChanged.subscribe(data => {
this.data = data;
});
}
ngOnDestroy() {
if (this.dataSubs) {
this.dataSubs.unsubscribe();
}
}
}
コンポーネントのライフサイクルイベント OnInit の ngOnInit メソッド内でサブスクライブしています。
subscribe でコールバックされるファンクションには、データサービス内の next メソッドに渡した値が渡されます。
すなわち、ここでデータサービス内のデータを受け取ることができます。
なお、サブスクライブしたらその戻り値として Subscription が返ります。コンポーネントが破棄される時、すなわち OnDestroy イベントハンドラ ngOnDestroy にて、 unsubscribe しないとメモリリークが発生しますので忘れないようにしましょう。
表示部の HTML テンプレートは次の通りです。
app/add-data/show-data.component.html
<div class="list1">
<ul class="list-group">
<li *ngFor="let s of data" class="list-group-item">{{s}}</li>
</ul>
</div>
CSS は次の通りです。
app/add-data/show-data.component.css
.list1 {
width: 400px;
margin: 10px;
}
ここで紹介した方法はコードはやや長くなりますが、next に渡した値を subscribe でそのまま受け取るなど、 直接的な方法で比較的わかりやすい方法と言えると思います。
データ表示用コンポーネントの実装 その 2
〜 Observable をそのまま使う
もう1つの方法は、データサービスの dataChanged を、 Observable のまま参照する方法があります。
app/add-data/show-data.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from '../data.service';
import { Subscription, Observable } from 'rxjs';
@Component({
selector: 'app-show-data',
templateUrl: './show-data.component.html',
styleUrls: ['./show-data.component.css']
})
export class ShowDataComponent implements OnInit {
data$: Observable<string[]>;
constructor(private dataService: DataService) {}
ngOnInit() {
this.data$ = this.dataService.dataChanged;
}
}
この場合、ngOnInit にて string[] 型の Observable として受け取ります。
Observable を受ける時の慣例として変数に $ を付けています。
そして、HTML テンプレートでは async パイプで直接データを取得しています。
app/add-data/show-data.component.html
<div class="list1">
<ul class="list-group">
<li *ngFor="let s of data$ | async" class="list-group-item">{{s}}</li>
</ul>
</div>
データを受けた後、特に何も操作をしない場合はこれで十分で、この方法によって非常に簡便にコードを記述することができます。
app/add-data/show-data.component.css は同じです。
最後にこれらのコンポーネントを app.component.html から使います。
<app-add-data></app-add-data>
<app-show-data></app-show-data>
これで上で示したような画面が表示されるはずです。
以上、ここではデータサービスを実装して、それに対してデータを入力するコンポーネントと、データサービスからデータを受け取るコンポーネントを実装する簡単な例を紹介しました。