angularangular2-changedetectionangular2-pipe

How to re-trigger all pure pipes on all component tree in Angular 2


I have pure pipe TranslatePipe that translates phrases using LocaleService that has locale$: Observable<string> current locale. I also have ChangeDetectionStrategy.OnPush enabled for all my components including AppComponent. Now, how can I reload whole application when someone changes language? (emits new value in locale$ observable).

Currently, I'm using location.reload() after user switches between languages. And that's annoying, because whole page is reloaded. How can I do this angular-way with pure pipe and OnPush detection strategy?


Solution

  • 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.