【Angular】複数のコンポーネント間でデータを共有する方法

 Angularで開発をしているとコンポーネント間でデータをやり取りする必要が出てくる場合があります。今回はそのコンポーネント間でデータをやり取りする方法を以下の4つのパターンで解説していきます。

  • 親コンポーネント → 子コンポーネント
  • 子コンポーネント → 親コンポーネント
  • 同一ページにある依存関係のないコンポーネント
  • 別ページにある依存関係のないコンポーネント

 今回はプロジェクトの作成やコンポーネントの生成などは飛ばします。コンポーネントは下記の図のようなイメージで作成します。

環境

  • node: 18.20.0
  • Angular CLI:17.0.0

親コンポーネント → 子コンポーネント

 親コンポーネントから子コンポーネントにデータを渡すには @Input() を使用します。

親コンポーネント

<mat-card>
  <mat-card-header>
    <p>page-a works!</p>
  </mat-card-header>
  <mat-card-content>
    <form>
      <mat-form-field>
        <input matInput [(ngModel)]="publisher" [ngModelOptions]="{standalone: true}">
      </mat-form-field>
    </form>

    <app-child-a [receiver]="publisher"></app-child-a>
  </mat-card-content>
</mat-card>
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

import { ChildAComponent } from '../../share/child-a/child-a.component';

@Component({
  selector: 'app-page-a',
  standalone: true,
  imports: [FormsModule, MatCardModule, MatFormFieldModule, MatInputModule, ChildAComponent],
  templateUrl: './page-a.component.html',
  styleUrl: './page-a.component.css'
})
export class PageAComponent {
  public publisher: string = '';
}

子コンポーネント

<mat-card>
  <mat-card-header>
    <p>child-a works!</p>
  </mat-card-header>
  <mat-card-content>
    {{ receiver }}
  </mat-card-content>
</mat-card>
import { Component, Input, input } from '@angular/core';
import { MatCardModule } from '@angular/material/card';

@Component({
  selector: 'app-child-a',
  standalone: true,
  imports: [MatCardModule],
  templateUrl: './child-a.component.html',
  styleUrl: './child-a.component.css'
})
export class ChildAComponent {
  @Input() receiver: string = '';
}

解説

 子コンポーネントで @Input() を使用して receiver を宣言します。 @Input() で宣言された変数は、親コンポーネントからこのコンポーネントを呼び出すときに属性として使用することができるようになります。

 このコードでは、親コンポーネントHTMLの <app-child-a [receiver]="publisher">receiver を属性として使用し、親コンポーネントの publisher と紐づけを行っています。

子コンポーネント → 親コンポーネント

 子コンポーネントから親コンポーネントにデータを渡すには @Output() を使用します。

親コンポーネント

<mat-card>
  <mat-card-header>
    <p>page-b works!</p>
  </mat-card-header>
  <mat-card-content>
    {{ receiver }}

    <app-child-b (shareEvent)="share($event)"></app-child-b>
  </mat-card-content>
</mat-card>
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';

import { ChildBComponent } from '../../share/child-b/child-b.component';

@Component({
  selector: 'app-page-b',
  standalone: true,
  imports: [MatCardModule, ChildBComponent],
  templateUrl: './page-b.component.html',
  styleUrl: './page-b.component.css'
})
export class PageBComponent {
  public receiver: string = '';

  public share(event: any) {
    this.receiver = event;
  }
}

子コンポーネント

<mat-card>
  <mat-card-header>
    <p>child-b works!</p>
  </mat-card-header>
  <mat-card-content>
    <form>
      <mat-form-field>
        <input matInput [(ngModel)]="publisher" [ngModelOptions]="{standalone: true}">
      </mat-form-field>
    </form>
  </mat-card-content>
  <mat-card-actions>
    <button mat-flat-button color="primary" (click)="onShare()">Share</button>
  </mat-card-actions>
</mat-card>
import { Component, EventEmitter, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-child-b',
  standalone: true,
  imports: [FormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule],
  templateUrl: './child-b.component.html',
  styleUrl: './child-b.component.css'
})
export class ChildBComponent {
  public publisher: string = '';

  @Output() shareEvent = new EventEmitter<string>();

  public onShare() {
    this.shareEvent.emit(this.publisher);
  }
}

解説

 子コンポーネントで @Output() を使用して shareEvent を宣言します。 @Output() で宣言された変数はそれ自体が親コンポーネントで使用することができるイベントとなっていて、そのイベントで emit されたデータを親コンポーネントで受け取ることで子コンポーネントから親コンポーネントにデータを渡すことができます。

 このコードでは、親コンポーネントHTMLの <app-child-b (shareEvent)="share($event)"> で子コンポーネントの shareEvent と親コンポーネントの share() メソッドを紐づけています。

同一ページにある依存関係のないコンポーネント

 依存関係がないコンポーネント間では直接データのやりとりはできないので、サービス経由でデータをやりとりします。

サービス

import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ShareService {

  private messageSubject: Subject<string> = new Subject();
  private massageState: Observable<string> = this.messageSubject.asObservable();

  constructor() { }

  public nextMessage(message: string) {
    this.messageSubject.next(message);
  }

  public subscribeMessage$(): Observable<string> {
    return this.massageState;
  }
}

child-c

<mat-card>
  <mat-card-header>
    <p>child-c works!</p>
  </mat-card-header>
  <mat-card-content>
    <form>
      <mat-form-field>
        <input matInput [(ngModel)]="publisher" [ngModelOptions]="{standalone: true}">
      </mat-form-field>
    </form>
  </mat-card-content>
  <mat-card-actions>
    <button mat-flat-button color="primary" (click)="onShare()">Share</button>
  </mat-card-actions>
</mat-card>
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

import { ShareService } from '../../../service/share.service';

@Component({
  selector: 'app-child-c',
  standalone: true,
  imports: [FormsModule, MatCardModule, MatFormFieldModule, MatInputModule, MatButtonModule],
  templateUrl: './child-c.component.html',
  styleUrl: './child-c.component.css'
})
export class ChildCComponent {
  public publisher: string = '';

  constructor(
    private shareService: ShareService
  ) { }

  public onShare() {
    this.shareService.nextMessage(this.publisher);
  }
}

child-d

<mat-card>
  <mat-card-header>
    <p>child-d works!</p>
  </mat-card-header>
  <mat-card-content>
    {{ receiver }}
  </mat-card-content>
</mat-card>
import { Component } from '@angular/core';

import { MatCardModule } from '@angular/material/card';

import { ShareService } from '../../../service/share.service';

@Component({
  selector: 'app-child-d',
  standalone: true,
  imports: [MatCardModule],
  templateUrl: './child-d.component.html',
  styleUrl: './child-d.component.css'
})
export class ChildDComponent {
  public receiver: string = '';

  constructor(
    private shareService: ShareService
  ) {
    this.share();
  }

  private share() {
    this.shareService.subscribeMessage$().subscribe((message: string) => {
      this.receiver = message;
    });
  }
}

解説

 サービスで RxJS を使用してデータを連携します。 RxJS は簡単に言えば非同期でデータを扱うライブラリです。 Subject で宣言した messageSubject にデータを流し ( next ) 、 messageSubject にデータが流れたタイミングで massageState を使用してデータを受け取る ( subscribe ) ことができます。雑な説明ですが、とりあえずデータを扱うにはこのぐらいの認識で大丈夫です。

他の方法

 同一ページ内でデータを連携するには他にも方法があります。

 例えば、 child-cpage-cchild-d と親コンポーネントを経由すればデータをやり取りすることができます。また、次で説明する setter と getter を使用してデータをやり取りすることもできます。ただこの場合は、 child-c に setter を呼び出すボタン、 child-d に getter を呼び出すボタンが必要になります。

別ページにある依存関係のないコンポーネント

 この場合もサービス経由でデータをやり取りしますが、この場合は setter と getter を使用します。

サービス

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ShareService {

  private message: string = '';

  constructor() { }

  public setMessage(message: string) {
    this.message = message;
  }

  public getMessage() {
    return this.message;
  }
}

page-d

<mat-card>
  <mat-card-header>
    <p>page-d works!</p>
  </mat-card-header>
  <mat-card-content>
    <form>
      <mat-form-field>
        <input matInput [(ngModel)]="publisher" [ngModelOptions]="{standalone: true}">
      </mat-form-field>
    </form>
  </mat-card-content>
</mat-card>
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

import { ShareService } from '../../../service/share.service';

@Component({
  selector: 'app-page-d',
  standalone: true,
  imports: [FormsModule, MatCardModule, MatFormFieldModule, MatInputModule],
  templateUrl: './page-d.component.html',
  styleUrl: './page-d.component.css'
})
export class PageDComponent {
  public publisher: string = '';

  constructor(
    private shareService: ShareService
  ) { }

  ngOnDestroy() {
    this.shareService.setMessage(this.publisher);
  }

}

page-e

<mat-card>
  <mat-card-header>
    <p>page-e works!</p>
  </mat-card-header>
  <mat-card-content>
    {{ receiver }}
  </mat-card-content>
</mat-card>
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';

import { ShareService } from '../../../service/share.service';

@Component({
  selector: 'app-page-e',
  standalone: true,
  imports: [MatCardModule],
  templateUrl: './page-e.component.html',
  styleUrl: './page-e.component.css'
})
export class PageEComponent {
  public receiver: string = '';

  constructor(
    private shareService: ShareService
  ) {
    this.receiver = this.shareService.getMessage();
  }
}

解説

 サービスで setter と getter を宣言して、それを使用してデータをやり取りしているだけです。今回はページ遷移のタイミング ( ngOnDestroy ) でしかデータを入れていませんが、ボタンで setter を呼び出すことも可能です。

 また、このコードですと page-epage-d とページ遷移するとデータが失われますが、 page-d のコンストラクターでデータを受け取ればそのまま同じデータを使い回すことも可能です。

最後に

 今回は簡単に RxJS の説明をしましたが、RxJS は Angular において重要な要素なのでもう少ししっかり勉強したいです。RxJS を使用する場面は MQTT などで外部からデータを受けるときによく使う印象です。しっかり勉強して RxJS を使いこなせるようにしたいですね。

コメント

タイトルとURLをコピーしました