Thanks to Günter Zöchbauer answer (see comments), I got it working.
As I understant, Angular's change detector works like this:
cd.detectChanges(); // Detects changes but doesn't update view.
cd.markForCheck(); // Marks view for check but doesn't detect changes.
So you need to use both in order to quickly rebuild whole component tree.
1. Template changes
In order to reload whole application we need to hide and show all component tree, therefore we need to wrap everything in app.component.html
into ng-container
:
<ng-container *ngIf="!reloading">
<header></header>
<main>
<router-outlet></router-outlet>
</main>
<footer></footer>
</ng-container>
ng-container
is better than div because it doesn't render any elements.
For async support, we can do something like this:
<ng-container *ngIf="!(reloading$ | async)"> ... </ng-container>
reloading: boolean
and reloading$: Observable<boolean>
here indicates that the component is currently being reloaded.
In the component I have LocaleService
which has language$
observable. I will listen to changed language event and perform application reload action.
2. Sync example
export class AppComponent implements OnInit {
reloading: boolean;
constructor(
private cd: ChangeDetectorRef,
private locale: LocaleService) {
this.reloading = false;
}
ngOnInit() {
this.locale.language$.subscribe(_ => {
this.reloading = true;
this.cd.detectChanges();
this.reloading = false;
this.cd.detectChanges();
this.cd.markForCheck();
});
}
}
3. Async example
export class AppComponent implements OnInit {
reloading: BehaviorSubject<boolean>;
get reloading$(): Observable<boolean> {
return this.reloading.asObservable();
}
constructor(
private cd: ChangeDetectorRef, // We still have to use it.
private locale: LocaleService) {
this.reloading = new BehaviorSubject<boolean>(false);
}
ngOnInit() {
this.locale.language$.subscribe(_ => {
this.reloading.next(true);
this.cd.detectChanges();
this.reloading.next(false);
this.cd.detectChanges();
});
}
}
We don't have to cd.markForChanges()
now but we still have to tell the detector to detect changes.
4. Router
Router doesn't work as expected. When reloading application in such fashion, router-outlet
content will become empty. I did not resolve this problem yet, and going to the same route can be painful because this means that any changes user has made in forms, for example, will be altered and lost.
5. OnInit
You have to use the OnInit hook. If you try to call cd.detectChanges() inside of constructor, you will get an error because angular will not build component yet, but you will try to detect changes on it.
Now, you may think that I subscribe to another service in constructor, and my subscription will only fire after component is fully initialized. But the thing is - you don't know how the service works! If, for example, it just emits a value Observable.of('en')
- you'll get an error because once you subscribe - first element emitted immediately while component is still not initialized.
My LocaleService
has the very same issue: the subject behind observable is BehaviorSubject
. BehaviorSubject
is rxjs subject that emits default value immediately right after you subscribe. So once you write this.locale.language$.subscribe(...)
- subscription immediately fires at least once, and only then you will wait for language change.
<ng-container *ngIf="!reloading">
. It was added to be able to use the more common syntax, but still with the same behavior as the<template>
element. – Halftrack