I have pure pipe TranslatePipe
that translates phrases using LocaleService
that has locale$: Observable
current locale. I al
Just set the property pure to false
@Pipe({
name: 'callback',
pure: false
})
Read this angular documentation for detect changes on array that is going to use pipe.
To fix that, create an new array(with new changes) and assign that to you array that you want to use in pipe. This time Angular detects that the array reference has changed. It executes the pipe and updates the display with the new array with new changes
enter link description here
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)
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.
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.
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.
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();
});
}
}
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.
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.
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.
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);
}
}