How to re-trigger all pure pipes on all component

2019-04-20 00:28发布

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?

5条回答
一夜七次
2楼-- · 2019-04-20 00:44

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.

查看更多
再贱就再见
3楼-- · 2019-04-20 00:44

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);
  }

}
查看更多
再贱就再见
4楼-- · 2019-04-20 00:45

Just set the property pure to false

@Pipe({
  name: 'callback',
  pure: false
})
查看更多
劫难
5楼-- · 2019-04-20 00:59

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.

查看更多
等我变得足够好
6楼-- · 2019-04-20 01:07

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)

查看更多
登录 后发表回答