noxi雑記

.NET、Angularまわりの小ネタブログ

Angular9では動的に生成するComponentをentryComponentsに追加しなくて良い

Angular9 の変更点の1つとして entryComponentsの非推奨 があります。
詳しい理由は以下の記事に記述されていますが、 Ivy を有効にしていると entryComponents に追加した Component で無くても CompnentFactorey を利用できるようになったからです。

dev.to

では本当に不要になったのか、 Angular Material の Dialog で試してみます。

環境

Angular CLI: 9.0.2
Node: 12.16.0
OS: win32 x64

Angular: 9.0.1
... animations, common, compiler, compiler-cli, core, forms     
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------     
@angular-devkit/architect         0.900.2
@angular-devkit/build-angular     0.900.2
@angular-devkit/build-optimizer   0.900.2
@angular-devkit/build-webpack     0.900.2
@angular-devkit/core              9.0.2
@angular-devkit/schematics        9.0.2
@angular/cdk                      9.0.0
@angular/cli                      9.0.2
@angular/material                 9.0.0
@ngtools/webpack                  9.0.2
@schematics/angular               9.0.2
@schematics/update                0.900.2
rxjs                              6.5.4
typescript                        3.7.5
webpack                           4.41.2

ダイアログの表示

適当なコンポーネントを作成し、ダイアログを表示させます。

// src/app/app.component.ts

import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { SampleComponent } from './sample/sample.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  constructor(private readonly _dialog: MatDialog) {
  }

  showDialog() {
    this._dialog.open(SampleComponent);
  }

}
// src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { SampleComponent } from './sample/sample.component';

@NgModule({
  declarations: [
    AppComponent,
    SampleComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatDialogModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

entryComponents に表示対象のコンポーネントを追加していませんが、無事表示されました。

f:id:noxi515:20200216175908p:plain

終わりに

Angular8 まではダイアログみたいな動的に生成されるコンポーネントを使用する場合 entryComponents に追加する必要がありましたが、 Angular9 の Ivy 環境では不要になりました。
今までは LazyModule で使用されるサービスが LazyModule に所属するコンポーネントをダイアログとして表示する場合に、サービスの所属を provideIn: 'root' から特定のモジュールに変更する必要がありました。これはとてもとても面倒で、循環参照の発生や LazyModule 間で依存があった場合の更なるモジュール分割など、手元のプロジェクトでは色々な問題が生まれていました。
Angular9 に更新することで LazyModule の EntryComponents 問題に悩まなくても良くなるのはとても嬉しいです。

Angularライブラリーに別のEntryPointを追加する

Angular CLI で作成したライブラリープロジェクトに別の EntryPoint を追加する方法です。
前回は Component Harness を使用したテストを書いてみましたが、 Component Harness はライブラリーとして配布することでより効果を得ることができます。しかし同じ EntryPoint からテスト用のクラスをインポート出来てしまうのは違和感があるため、 @hoge/lib に対して @hoge/lib/testing を追加してみます。

続きを読む

RxJSのfromEventとfromEventPattern

RxJS にはイベントハンドリングをする API が2種類あります。 fromEventfromEventPattern です。前者は DOM イベントを処理するための API で、後者は任意のイベントハンドラーに対して addremove を手動で処理する API です。

例えば KeyboardEvent を処理するときに、やっていることは同じですが2種類の書き方ができます。

const destroy$: Subject<void> = new Subject<void>();
const input: HTMLInputElement = document.createElement('input');

// fromEventを使用してDOMイベントを処理する
fromEvent<KeyboardEvent>(input, 'keydown')
  .pipe(
    filter(ev => ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)),
    takeUntil(destroy$),
  )
  .subscribe(ev => {
    // なにか
  });

// fromEventPatternを使用してDOMイベントを処理する
fromEventPattern<KeyboardEvent>(
  handler => input.addEventListener('keydown', handler),
  handler => input.removeEventListener('keydown', handler))
  .pipe(
    filter(ev => ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)),
    takeUntil(destroy$),
  )
  .subscribe(ev => {
    // なにか
  });

ちなみに fromEvent は DOM の addEventListener/removeEventListenerjQueryon/off 、 Node.js の addListener/removeListener の組み合わせに対して使用できます。逆に言うとこれ以外の組み合わせに対してイベントハンドリングをしたい場合は fromEventPattern を使用します。

OnPushに設定したComponentのテストを実装する

Angular の高パフォーマンスな Component を実装する上で重要なのは ChangeDetection: ChangeDetectionStrategy.OnPush に設定して不要な変更チェックを走らせないようにすることが上げられます。しかしこれを設定すると Component のテストを書く時に変更されたプロパティが DOM に反映されず辛い思いをします。

そこで便利なのがこの関数。元は Github の Issue にあったものですが、これによって OnPush Component の変更反映を待つことができ、期待したテスト結果を得られます。

github.com

async function runOnPushChangeDetection<T>(cf: ComponentFixture<T>): Promise<any> {
  const cd = cf.debugElement.injector.get(ChangeDetectorRef);
  cd.detectChanges();
  return await cf.whenStable();
}