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

18,769

Solution 1

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

Solution 2

Pure pipes are only triggered when the input value changes.

You could add an artificial additional parameter value that you modify

@Pipe({name: 'translate'})
export class TranslatePipe {
  transform(value:any, trigger:number) {
    ...
  }
}

and then use it like

<div>{{label | translate:dummyCounter}}</div>

Whenever dummyCounter is updated, the pipe is executed.

You can also pass the locale as additional parameter instead of the counter. I don't think using |async for a single pipe parameter will work, therefore this might a bit cumbersome (would need to be assigned to a field to be usable as pipe parameter)

Solution 3

BEST PERFORMANCE SOLUTION:

I figured out a solution for this. I hate to call it a solution, but it works.

I was having the same issue with and orderBy pipe. I tried all the solutions here but the performance impact was terrible.

I simply added an addtional argument to my pipe

let i of someArray | groupBy:'someField':updated" 
<!--updated is updated after performing some function-->

then anytime I perform an update to the array I simply to

updateArray(){
    //this can be a service call or add, update or delete item in the array
      .then.....put this is in the callback:

    this.updated = new Date(); //this will update the pipe forcing it to re-render.
}

This forces my orderBy pipe to do a transform again. And the performance is a lot better.

Solution 4

Just set the property pure to false

@Pipe({
  name: 'callback',
  pure: false
})

Solution 5

You can also create your own unpure pipe to track external changes. Check the sources of native Async Pipe to get the main idea.

All you need is to call ChangeDetectorRef.markForCheck(); inside of your unpure pipe every time your Observable return new locale string. My solution:

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private subscription: Subscription;
  private lastInput: string;
  private lastOutput: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.subscription = this.globalizationService.currentLocale // <- Observable of your current locale
      .subscribe(() => {
        this.lastOutput = this.globalizationService.translateSync(this.lastInput); // sync translate function, will return string
        this.changeDetectorRef.markForCheck();
      });
  }

  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
    this.subscription = void 0;
    this.lastInput = void 0;
    this.lastOutput = void 0;
  }

  transform(id: string): string { // this function will be called VERY VERY often for unpure pipe. Be careful.
    if (this.lastInput !== id) {
      this.lastOutput = this.globalizationService.translateSync(id);
    }
    this.lastInput = id;
    return this.lastOutput;
  }
}

Or you even can incapsulate AsyncPipe inside your pipe (not a good solution, just for example):

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private asyncPipe: AsyncPipe;
  private observable: Observable<string>;
  private lastValue: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.asyncPipe = new AsyncPipe(changeDetectorRef);
  }

  ngOnDestroy(): void {
    this.asyncPipe.ngOnDestroy();
    this.lastValue = void 0;
    if (this.observable) {
      this.observable.unsubscribe();
    }
    this.observable = void 0;
    this.asyncPipe = void 0;
  }

  transform(id: string): string {
    if (this.lastValue !== id || !this.observable) {
      this.observable = this.globalizationService.translateObservable(id); // this function returns Observable
    }
    this.lastValue = id;

    return this.asyncPipe.transform(this.observable);
  }

}
Share:
18,769

Related videos on Youtube

Ivan Zyranau
Author by

Ivan Zyranau

Coder. Developer. Psychologist. Optimist. :).

Updated on June 04, 2022

Comments

  • Ivan Zyranau
    Ivan Zyranau about 2 years

    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?

  • Ivan Zyranau
    Ivan Zyranau over 7 years
    Thanks, that's one way to solve this problem, but I have lots of strings in my templates and lots of code like {{'This is some text' | translate}}, so having an additional parameter that I'd have to put everywhere will hurt... Isn't there any way to reload whole app without reloading whole page?
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    You can put an ngIf="expr" at the outermost element of your AppComponent, change expr to false, run ChangeDetectorRef.detectChanges(), set it back to true and run detectChanges() again. I think this is the simplest way. This way everything will be removed and readded. You might need to navigate to the current route again (not sure how the router reacts to this at all though, but I think it's easy enough to just try it)
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    You can use <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.
  • Ivan Zyranau
    Ivan Zyranau over 7 years
    Updated using <ng-container>.
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    AFAIK cd.detectChanges(); runs change detection immediately and synchronously and cd.markForCheck(); runs change detection for this component on the next change detection cycle (somewhat later).
  • Ivan Zyranau
    Ivan Zyranau over 7 years
    When I tried to use cd.detectChanges() without cd.markForCheck() at the end - it just doesn't work. It doesn't work either with only cd.markForCheck().
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    Might be related to the router. Perhaps ApplicationRef.tick() is a better way in this case to invoke change detection for the whole application. This might even fix 4.
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    Your 5. is a bit weird. If this code is in the constructor, this.reloading.next() and the following code will be called at the same time as when you move it to ngOnInit. It will be called when this.locale.language$.subscribe(...) emits a event.
  • Ivan Zyranau
    Ivan Zyranau over 7 years
    ApplicationRef.tick() didn't do anything either) That's the first thing I tried when this issue arose. Anyway, I posted what works for me, now I'm trying to struggle with the Router. If I'll come to a more beautiful solution I'll update the unswer. Maybe angular should include some method like ApplicationRef.updateWholeComponentTreeWithAllRouters() ?
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    Ok. Thans for the write-up anyway :)
  • Ivan Zyranau
    Ivan Zyranau over 7 years
    Also, I can't to force router-outlet to be updated. If I navigate to the same route or event to another route it's still empty (after my detecting changes).
  • Günter Zöchbauer
    Günter Zöchbauer over 7 years
    Hard to say. Could you try to reproduce in a Plunker? (as little code as possible, just as much to demonstrate the issue) Plunker provides a ready-to-use Angular2 TS template.
  • StinkyCat
    StinkyCat over 6 years
  • Renil Babu
    Renil Babu over 6 years
    Worked..! Thanks
  • skyboyer
    skyboyer almost 6 years
    @StinkyCat checking translation is just reading from hashmap. can it affect performance so bad?
  • StinkyCat
    StinkyCat almost 6 years
    It depends on the website you're building, but if it's a huge website, with lots of bindings... yes it can be one more thing to slow down the experience. (my opinion, from experience, not actually measured!)
  • Bryan Rayner
    Bryan Rayner over 5 years
    He specifically asked for a pure pipe.
  • Harry Burns
    Harry Burns over 5 years
    Bryan, I know. But Async Pipe solution is good enough to create translation pipes. At least, it works on my projects very well. No performance issues were noticed.
  • cambunctious
    cambunctious over 5 years
    Isn't markForCheck meaningless in a non-pure pipe? Or even any pipe?
  • Harry Burns
    Harry Burns over 5 years
    @cambunctious, if your pipe depends on something that is not a parameter of this pipe (for example, the application locale from localization service), then if that parameter changes, you need to force the pipe to call the transform() method again. Even if your ChangeDetectionStrategy is Default. ChangeDetectorRef.markForCheck() will do that. It will mark the current view to re-check changes, and that will call the transform() function.
  • cambunctious
    cambunctious over 5 years
    Okay, I see. This is a good solution since it prevents the component from having to use ChangeDetectorRef. AsyncPipe is a good related example since it is also implemented using ChangeDetectorRef, but you certainly don't need to use it. Also just to note, an impure pipe is absolutely necessary if you want a pipe to change its output without changing the input. As long as the transform method is inexpensive, it's okay, in the same way that it's okay to use default change detection and have angular access your component often.
  • CPHPython
    CPHPython almost 5 years
    Harry, an old alligator article actually recommended using markForCheck at the end of the subscribe block as you pointed out. However, this could be inside of ngOnInit instead of the constructor.
  • Florent Arlandis
    Florent Arlandis almost 5 years
    You Sir just saved my day. Thank you! :)
  • El7or
    El7or over 4 years
    Perfect solution, it's worked with me as the charm!!
  • mhombach
    mhombach over 2 years
    You don't need to send a new Date. Just send a new empty object {}. Angular checks the reference, so that is enough and waaaay less overhead, specifically when you label your solution as "performant".
  • mhombach
    mhombach over 2 years
    The title of the question is "How to re-trigger all pure pipes on all component tree in Angular 2". If your "answer" is "make it not pure" then you failed to understand the question. All we devs are aware of impure pipes are re-triggered a lot of times, that's not the question here.
  • mhombach
    mhombach over 2 years
    What you wrote has nothing to do with the question asked. The question is about PURE pipes.