データサービスとそのサブスクライブ

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.tsdeclarations にコンポーネントを追加します。

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.tsproviders に 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>

これで上で示したような画面が表示されるはずです。

以上、ここではデータサービスを実装して、それに対してデータを入力するコンポーネントと、データサービスからデータを受け取るコンポーネントを実装する簡単な例を紹介しました。