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 上に置いてあります。
実際に動作するサンプルページはこちらです。
本記事で紹介している 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 の学ぶべき項目が減り、またアプリケーションをよりシンプルに開発できるようになります。
この機能は Strictly Typed Reactive Forms 同様、 GitHub 上の Discussion で RFC が実施されていました。
Standalone Components でアプリを実装する
試してみる前に、 Angular プロジェクトを準備します。プロジェクトの作成には以下のコマンドを利用しました。見た目が無いと寂しいため Angular Material も追加してあります。
ng new ng-sample-standalone-app --strict --minimal cd ng-sample-standalone-app ng add @angular/material
プロジェクトを作成したら不要な AppComponent
と AppModule
は削除してしまいましょう。
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-outlet
と routerLink
を使用しますので RouterModule
、または RouterOutlet
と RouterLinkWithHref
の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.ts
の bootstrapApplication
で読み込めば完成です。
// 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 ではできませんでした。