Skip to content

Instantly share code, notes, and snippets.

@kazuma1989
Created December 31, 2017 03:36
Show Gist options
  • Save kazuma1989/1e1f8182af19b7bd571ed593d6b65629 to your computer and use it in GitHub Desktop.
Save kazuma1989/1e1f8182af19b7bd571ed593d6b65629 to your computer and use it in GitHub Desktop.
Angular の ErrorHandler からエラー表示コンポーネントを制御する
import { Injectable, ErrorHandler, NgZone } from '@angular/core';
import { MessageService } from '../../service/message/message.service';
@Injectable()
export class ErrorHandlerService implements ErrorHandler {
constructor(
private message: MessageService,
private zone: NgZone,
) { }
handleError(error: any) {
console.error('ERROR', error);
// handleError() はボタン操作などの UI イベントとは関係なく呼ばれるため、コンポーネントの変化が検知できず、画面が変化しないことがある。
// それを防止するため、NgZone#run() 内でサービスを呼び出している。
this.zone.run(() => {
this.message.showError(error.message);
});
}
}
export const ErrorHandlerServiceProvider = {
provide: ErrorHandler,
useClass: ErrorHandlerService
};
import { Component, OnInit } from '@angular/core';
import { MessageService, Message } from '../../service/message/message.service';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-message-box',
template: `
<ng-container *ngFor="let message of messageList" [ngSwitch]="message.type">
<div *ngSwitchCase="'info'" class="msg-box" (click)="message.remove()">
{{message.content}}
</div>
<div *ngSwitchCase="'error'" class="msg-box--error" (click)="message.remove()">
<i class="icon-error"></i>
{{message.content}}
</div>
</ng-container>
`,
styleUrls: ['./message-box.component.scss']
})
export class MessageBoxComponent implements OnInit {
messageList: any[];
constructor(
private messenger: MessageService,
// private cd: ChangeDetectorRef
) { }
ngOnInit() {
this.messageList = [];
this.messenger.currentMessage.subscribe(message => {
this.add(message);
// ChangeDetectorRef#detectChanges() で変更をビューに伝えてもよい。
// しかし、この呼び出しが必要なのは MessageService#showError() を ErrorHandler が呼んだ場合であって、
// コンポーネントや Resolve#resolve() が呼んだ場合には必要ない。
// 呼び出し元の ErrorHandler に責任があると考え、NgZone#run() で対応している。
// this.cd.detectChanges();
});
}
private add(message: Message) {
const { type, content } = message;
const list = this.messageList;
// 新しいメッセージが上に表示されるように、push() ではなく unshift() を使う
this.messageList.unshift({
type,
content,
remove() {
list.splice(list.indexOf(this), 1);
}
});
}
}
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
export interface Message {
type: 'info' | 'error';
content: string;
}
@Injectable()
export class MessageService {
currentMessage: Observable<Message>;
private messenger: Subject<Message>;
constructor() {
// Subject を公開し、サービスを使う側に subscribe() させることもできるが、
// Subject#next() を呼んで値を配信することもできてしまうので、Observable のみを公開プロパティとし、
// そちらに subscribe() させる。
this.messenger = new Subject<Message>();
this.currentMessage = this.messenger.asObservable();
}
showInfo(content: string) {
this.messenger.next({
type: 'info',
content
});
}
showError(content: string) {
this.messenger.next({
type: 'error',
content
});
}
}
@kazuma1989
Copy link
Author

Angular Guard の動作確認と、ローディング中表示の実装 では Resolve とコンポーネントを連携させた。
ErrorHandler とコンポーネントを連携させる際には、追加で NgZone#run() が必要だったので、備忘録として残す。

ErrorHandler は通常のサービスと異なり、コンポーネントに inject できない。
そのため MessageService を介してコンポーネントの表示を制御している。
ただし、handleError() の呼び出しはビュー変更イベントから漏れるらしく、明示的に NgZone#run() 内でサービスを呼び出さないと、コンポーネントが subscribe() 内でプロパティを変化させてもビューに反映されなかった。

コンポーネントの状態をビューに反映する方法として、もう一つ ChangeDetectorRef#detectChanges() を使う方法もあるが、今回の場合は呼び出し元の責任なので、NgZone を使うのが妥当だろう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment