Observable の状態に応じたUI表示を行う構造ディレクティブ

2024/12/07に公開

本記事は、Angularアドベントカレンダー7日目の記事です。

https://qiita.com/advent-calendar/2024/angular

はじめに

アプリケーション開発では、ネットワークリクエストや非同期処理などの結果に応じて

  • 読み込み済み
  • ローディング中
  • エラー

といったUIの切り替えが必要になる場面がよくあります。
ですがこれらを単純なフラグ管理で実装しようとすると毎回同じようなコードを書く必要があり、冗長になりがちです。

本記事では、RxJS の Observable の状態に基づいてUIを切り替えるための、カスタム構造ディレクティブを紹介します。
これにより非同期処理の状態管理をより簡単に書けるようになります。

使い方

npm install @twogate/ngx-suspense

https://www.npmjs.com/package/@twogate/ngx-suspense

https://github.com/twogate/ngx-suspense

基本的な使い方

<ng-container *ngxSuspense="source$; fallback: Fallback; error: Error">
  <!-- 読み込み完了時の表示 -->
</ng-container>
<ng-template #Fallback>
  <!-- ローディング中の表示 -->
</ng-template>
<ng-template #Error>
  <!-- エラー時の表示 -->
</ng-template>
import { Component } from '@angular/core';
import { timer } from 'rxjs';

import { SuspenseDirective } from '@twogate/ngx-suspense';

@Component({
  imports: [SuspenseDirective],
})
export class SomeComponent {
  source$ = timer(5000);
}

SuspenseDirective に対して、 Observable と Fallback Template と Error Template を渡すと、入力の Observable の値が流れてくるまでは Fallback Template を表示し、エラーが発生したら Error Template を表示します。値が流れてきたら中の Template を表示するようになります。

実際の挙動は以下のようになります。

デフォルトコンポーネントの注入

アプリケーション全体で統一したローディングやエラー表示を実現したい場面はよくあると思います。
そこで、デフォルトで表示できるコンポーネントを注入する仕組みがあります。

provideDefaultSuspenseFallbackComponentprovideDefaultSuspenseErrorComponent を使って、Fallback と Error それぞれに対してデフォルトのコンポーネント (と必要であればコンポーネントの入力) を注入します。

// app.config.ts
import { provideDefaultSuspenseFallbackComponent, provideDefaultSuspenseErrorComponent } from '@twogate/ngx-suspense';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideRouter(routes),
    // ...
    provideDefaultSuspenseFallbackComponent(DefaultFallbackComponent, { message: 'default message' }),
    provideDefaultSuspenseErrorComponent(DefaultErrorComponent, { message: 'default error' }),
  ],
};

// main.ts
bootstrapApplication(AppComponent, appConfig);

これで直接テンプレートを指定しない場合はデフォルトで注入されたコンポーネントが使われるようになります。

<ng-container *ngxSuspense="source$">
  <!-- 読み込み完了時の表示 -->
</ng-container>

テンプレート内で Observable に流れてきた値を使う

HTTP GET リクエストの Observable などに用いる場合、そのままリクエストの結果を表示することが多いと思います。 SuspenseDirective では、Observable の流れてきた値をテンプレートの中でそのまま使うことができます。

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { SuspenseDirective } from '@twogate/ngx-suspense';

@Component({
  imports: [SuspenseDirective],
})
export class SomeComponent {
  source$;
  private http = inject(HttpClient);
  constructor() {
    this.source$ = this.http.get<{
      id: number;
      userId: number;
      title: string;
      completed: boolean;
    }>('https://jsonplaceholder.typicode.com/todos/1');
  }
}

Observable に流れてきた値は template の context の value から値を受け取ることができます。
error の場合も同様に context の error からエラーの値を受け取ることができます。

<ng-container *ngxSuspense="source$; fallback: Fallback; error: Error; value as data">
  <p>id: {{ data.id }}</p>
  <p>title: {{ data.title }}</p>
</ng-container>

<ng-template #Fallback> loading... </ng-template>

<ng-template #Error let-error="error">
  <p>{{ error.message }}</p>
</ng-template>

実装の詳細

内部でやっている処理はシンプルです。 一部を抜き出して簡単に解説します。
SuspenseDirective では入力で Fallback と Error のテンプレートを受けとり、 inject(TemplateRef) が内部のテンプレートです。
Angular の構造ディレクティブでは、*ngIf*ngFor のように * を使った省略記法を使用できます。この記法を使用すると、内部的にはディレクティブ名がプレフィックスとして使用されるため[1]、それに対応できるよう @Input に alias を設定しています。

また ngTemplateContextGuard を使って、 Context の型情報を付与できます [2]。 これによりテンプレート内で context の値を型安全に扱うことができます。

https://github.com/twogate/ngx-suspense/blob/78b12b20f16afad2cf34e15172525e9c875a0fe8/projects/suspense/src/lib/suspense.directive.ts#L15-L53

具体的な処理は SuspenseBase 内で行っており、入力に渡された Observable を購読し、それぞれの状態に応じてテンプレートを表示します。
viewContainerRef.clear() でビューをクリアし、 TemplateRef の場合は viewContainerRef.createEmbeddedView() 、 コンポーネントの場合は viewContainerRef.createComponent() で表示します。
このあたりは NgIf とよく似ており、 NgIf の実装 を参考にしています。

https://github.com/twogate/ngx-suspense/blob/78b12b20f16afad2cf34e15172525e9c875a0fe8/projects/suspense/src/lib/suspense-base.ts#L40-L116

最後に

RxJS の Observable の状態に応じて UI を切り替えるライブラリ @twogate/ngx-suspense を紹介しました。
このライブラリを使用することで、非同期処理中のローディング表示やエラー表示などを、よりシンプルに記述できるようになります。

なお、Angular v19 では resourcerxResource といった新しい API が Experimental として追加されており、Signal ベースの新しいデータフェッチの仕組みによって、今後のコードの書き方が大きく変わっていく可能性があります。

また、今回は紹介しませんでしたが、 @twogate/ngx-suspense は構造ディレクティブだけでなく、コンポーネントとしても同様のことができるようになっています。
ぜひ試してみてください。

脚注
  1. https://angular.dev/guide/directives/structural-directives#structural-directive-syntax-reference ↩︎

  2. https://angular.dev/guide/directives/structural-directives#typing-the-directives-context ↩︎

TwoGate Tech Blog

Discussion