Angular Standalone Componentsを試してみよう

2022年6月にリリースされた Angular 14 には Standalone Components という新機能が含まれています。今回は Standalone Components のみを使用して小さなアプリケーションを開発してみます。


試した環境

Angular CLI: 14.2.3
Node: 16.17.0
Package Manager: npm 8.15.0
OS: win32 x64

Angular: 14.2.2
... animations, cdk, common, compiler, compiler-cli, core, forms
... material, platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1402.3
@angular-devkit/build-angular   14.2.3
@angular-devkit/core            14.2.3
@angular-devkit/schematics      14.2.3
@angular/cli                    14.2.3
@schematics/angular             14.2.3
rxjs                            7.5.6
typescript                      4.7.4

本記事に記載のコードは全て GitHub 上に置いてあります。
実際に動作するサンプルページはこちらです。

github.com

本記事で紹介している Standalone Components の機能は全て開発者プレビューです。
今後 API や機能が変更される可能性があります点、予めお断りしておきます。

Standalone Components とは

Standalone Components とは Angular 14 に含まれる新機能です(ただし14時点では開発者プレビューの扱いとなっています)。
Angular フレームワークでは Component, Directive, Pipe は全て Angular のモジュール機能である NgModule に含まれねばなりません。 NgModule は JavaScript のモジュール機能とは異なる Angular 専用機能ですので、この概念は Angular を学ぶ上で障壁の1つとなっていました。
Standalone Components は NgModule という枷を取り外して Component, Directive そして Pipe がそれぞれ単体で動作可能になる機能です。この機能によって Angular の学ぶべき項目が減り、またアプリケーションをよりシンプルに開発できるようになります。

angular.io

この機能は Strictly Typed Reactive Forms 同様、 GitHub 上の Discussion で RFC が実施されていました。

github.com

Standalone Components でアプリを実装する

試してみる前に、 Angular プロジェクトを準備します。プロジェクトの作成には以下のコマンドを利用しました。見た目が無いと寂しいため Angular Material も追加してあります。

ng new ng-sample-standalone-app --strict --minimal

cd ng-sample-standalone-app
ng add @angular/material

プロジェクトを作成したら不要な AppComponentAppModule は削除してしまいましょう。

Standalone Component を生成する

まずは Standalone な Component を1つ作成します。 CLI が既に Standalone Components をサポートしていますので、 Component を作成するコマンドに --standalone オプションを付与するのみです。

ng generate component app --standalone

このコマンドを実行すると以下の Component が作成されます。

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-root',  // ※index.htmlに合わせてselectorだけ変更
  standalone: true,
  imports: [CommonModule],
  template: `
    <p>
      app works!
    </p>
  `,
  styles: [
  ]
})
export class AppComponent implements OnInit {

  constructor() { }

  ngOnInit(): void {
  }

}

通常の Component との差は standalone: true というフラグが付与されている点と、 imports: [CommonModule] と普段ならば NgModule でインポートされているものを Component から直接インポートしている点の、2点です。 NgModule に所属せず自身の依存関係を単体で明確にする必要があるため、動作に必要な NgModule, Component, Directive, そして Pipe を全て imports に含めることが必須です。

ちょっと見た目をリッチにするために、 Material の Drawer を入れておきましょう。 MatSidenavModule のインポートを忘れるとコンパイラーに怒られますので、忘れずに追加します。

import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatSidenavModule } from '@angular/material/sidenav';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CommonModule,
    MatSidenavModule,
  ],
  template: `
    <mat-drawer-container>
      <mat-drawer mode="side" opened></mat-drawer>
      <mat-drawer-content>
        <main class="mat-app-background"></main>
      </mat-drawer-content>
    </mat-drawer-container>
  `,
  styles: [`
    .mat-drawer-container {
      height: 100%;
    }

    .mat-drawer {
      width: 250px;
    }
  `]
})
export class AppComponent implements OnInit {
  ...
}

Standalone Component を bootstrap に設定する

続いて先に作成した Component でアプリケーションを起動するため、 bootstrap に設定します。 bootstrap の設定を行うのは main.ts です。

import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app/app.component';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

// NgModuleのBootstrap
// platformBrowserDynamic().bootstrapModule(AppModule)
//   .catch(err => console.error(err));

// StandaloneのBootstrap
bootstrapApplication(AppComponent)
  .catch(err => console.error(err));

NgModule を bootstrap に設定するには platformBrowserDynamic().bootstrapModule としていましたが、 Standalone な Component を bootstrap に設定するには、 bootstrapApplication を使用します。これだけで Standalone Components を使用したアプリが起動するようになります。

今回のサンプルでは Angular Material を使用しており、 BrowserAnimationModule または NoopAnimationModule のインポートが必要です。アプリ全体に関わるインポートは今までは AppModule で行っていましたが、 Standalone Components においては bootstrapApplication の第二引数で設定します。アニメーション機能を読み込むためにそれぞれ provideAnimations provideNoopAnimations という関数が新設されていますので、それを利用しましょう。なおアニメーション等専用の関数が用意されていない機能を NgModule からインポートする場合は importProvidersFrom を使用します。

bootstrapApplication(AppComponent, {
  providers: [
    provideAnimations(),  // or provideNoopAnimations()
  ]
})
  .catch(err => console.error(err));

ルーティングと LazyLoad を設定する

最後にルーティングについてです。今回のサンプルではルートのページとサブ2ページ、合わせて3ページ分の設定を行います。

まずは先に作成した AppComponent に router-outlet と各ページへのリンクを配置します。 router-outletrouterLink を使用しますので RouterModule 、または RouterOutletRouterLinkWithHref の2つをインポートに追加します。

import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatListModule } from '@angular/material/list';
import { MatSidenavModule } from '@angular/material/sidenav';
import { RouterModule } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CommonModule,
    MatListModule,
    MatSidenavModule,
    RouterModule,
  ],
  template: `
    <mat-drawer-container>
      <mat-drawer mode="side" opened>
        <mat-nav-list>
          <a mat-list-item [routerLink]="['/']">Root Page</a>
          <a mat-list-item [routerLink]="['/', 'first']">First Page</a>
          <a mat-list-item [routerLink]="['/', 'second']">Second Page</a>
        </mat-nav-list>
      </mat-drawer>
      <mat-drawer-content>
        <main class="mat-app-background">
          <router-outlet></router-outlet>
        </main>
      </mat-drawer-content>
    </mat-drawer-container>
  `,
  styles: [`
    .mat-drawer-container {
      height: 100%;
    }

    .mat-drawer {
      width: 250px;
    }
  `]
})
export class AppComponent implements OnInit {
  ...
}

続いてページ用 Component を作成します。こちらは CLI のコマンドを叩くだけですね。

ng generate component root-page --standalone
ng generate component first-page --standalone
ng generate component second-page --standalone

最後にルーティングの設定です。 Component 単位で LazyLoad するための loadComponent が新設されていますので、ルーティング設定の大小に合わせて従来のグループで読み込む loadChildren か単体で読み込む loadComponent かを選択すれば良いと思います。今回は loadComponent を使用しています。

import { Routes } from '@angular/router';
import { RootPageComponent } from './root-page/root-page.component';

export const routes: Routes = [
  {
    path: '',
    component: RootPageComponent,
  },
  {
    path: 'first',
    loadComponent: () => import('./first-page/first-page.component').then(x => x.FirstPageComponent),
  },
  {
    path: 'second',
    loadComponent: () => import('./second-page/second-page.component').then(x => x.SecondPageComponent),
  },
];

作成したルーティングの定義を main.tsbootstrapApplication で読み込めば完成です。

// StandaloneのBootstrap
bootstrapApplication(AppComponent, {
  providers: [
    provideAnimations(),
    provideRouter(routes),
  ]
})
  .catch(err => console.error(err));

おわりに

Angular は全ての Component や Directive が NgModule 単位で管理され、また NgModule がルーティングの単位にもなるため、 Component や Directive のモジュール間共有に謎の SharedModule や CommonModule が発生しがちだと思います。(筆者だけですかね。。?)
NgModule で管理することにも利点はありますが、シナリオによってはこの機能によって、よりシンプルに Angular アプリケーションが開発できそうな予感がします。

ちなみに執筆時点(2022年9月)では、 WebStorm では Alt + Enter で Component の imports に不足しているモジュールを自動で追加することができますが、 VSCode ではできませんでした。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧