Using getters in Angular Component Templates? - javascript

Suppose we have a getter defined like this:
get sku() {
return this.form.get('sku').value
}
And we use it like this:
<mat-hint *ngIf="!sku">Example sku123</mat-hint>
Is there a better way of doing this from an Angular Change detection performance perspective?
We could use an Observable. Something like this I think (Roughing this out):
sku$ = this.form.get('sku').valueChanges().pipe(untilDestroyed(this))
<mat-hint *ngIf="!(sku|async)">Example sku123</mat-hint>
Is one better than the other?
IIUC the getter will be called whenever there is change detection, but if we use ChangeDetectionStrategy.OnPush with the Observable then will only receive notifications when the form updates?

using ChangeDetectionStrategy.OnPush will not apply the change to your sku in the view *ngIf because automatic change detection is deactivated now. Only the event will be triggered without changing the view. Therefore, you will have to explicitly invoke it using,
constructor(private cdRef: ChangeDetectorRef) {}
this.form.valueChanges
.subscribe(() => {
this.cdRef.markForCheck();
});
}
<mat-hint *ngIf="!sku">Example sku123</mat-hint>
Note: Performance will be increased by using ChangeDetectionStrategy.OnPush as it only runs the cycle once. But will get complicated when it comes to reactive forms. And also all the child components inside the parent component using ChangeDetectionStrategy.OnPush will be inherited and you will have to invoke them manually if needed.

Getter is fine for the performance, except if the function is expensive as it will be called at each cycle. In this case, a pipe is preferred as the result is cached.

Related

Angular - RxJS : afterViewInit and Async pipe

I tried to do the following in my component which uses changeDetection: ChangeDetectionStrategy.OnPush,
#ViewChild('searchInput') input: ElementRef;
ngAfterViewInit() {
this.searchText$ = fromEvent<any>(this.input.nativeElement, 'keyup')
.pipe(
map(event => event.target.value),
startWith(''),
debounceTime(300),
distinctUntilChanged()
);
}
And in the template
<div *ngIf="searchText$ | async as searchText;">
results for "<b>{{searchText}}</b>"
</div>
It doesn't work, however if I remove the OnPush, it does. I am not too sure why since the async pipe is supposed to trigger the change detection.
Edit:
Following the answers, I have tried to replace what I have by the following:
this.searchText$ = interval(1000);
Without any #Input, the async pipe is marking my component for check and it works just fine. So I don't get why I haven't got the same behavior with the fromEvent
By default Whenever Angular kicks change detection, it goes through all components one by one and checks if something changes and updates its DOM if it's so. what happens when you change default change detection to ChangeDetection.OnPush?
Angular changes its behavior and there are only two ways to update component DOM.
#Input property reference changed
Manually called markForCheck()
If you do one of those, it will update DOM accordingly. in your case you don't use the first option, so you have to use the second one and call markForCheck(), anywhere. but there is one occasion, whenever you use async pipe, it will call this method for you.
The async pipe subscribes to an Observable or Promise and returns the
latest value it has emitted. When a new value is emitted, the async
pipe marks the component to be checked for changes. When the component
gets destroyed, the async pipe unsubscribes automatically to avoid
potential memory leaks.
so there is nothing magic here, it calls markForCheck() under the hood. but if it's so why doesn't your solution work? In order to answer this question let's dive in into the AsyncPipe itself. if we inspect the source code AsyncPipes transform function looks like this
transform(obj: Observable<any>|Promise<any>|null|undefined): any {
if (!this._obj) {
if (obj) {
this._subscribe(obj);
}
this._latestReturnedValue = this._latestValue;
return this._latestValue;
}
....// some extra code here not interesting
}
so if the value passed is not undefined, it will subscribe to that observable and act accordingly (call markForCheck(), whenever value emits)
Now it's the most crucial part
the first time Angular calls the transform method, it is undefined, because you initialize searchText$ inside ngAfterViewInit() callback (the View is already rendered, so it calls async pipe also). So when you initialize searchText$ field, the change detection already finished for this component, so it doesn't know that searchText$ has been defined, and subsequently it doesn't call AsyncPipe anymore, so the problem is that it never get's to AsyncPipe to subscribe on those changes, what you have to do is call markForCheck() only once after the initialization, Angular ran changeDetection again on that component, update the DOM and call AsyncPipe, which will subscribe to that observable
ngAfterViewInit() {
this.searchText$ =
fromEvent<any>(this.input.nativeElement, "keyup").pipe(
map((event) => event.target.value),
startWith(""),
debounceTime(300),
distinctUntilChanged()
);
this.cf.markForCheck();
}
The changeDetection: ChangeDetectionStrategy.OnPush allow to the component to not triggered the changeDetection all the time but just when an #Input() reference is updated. So if you do all your stuff in the same component, no #Input() reference are updated so the view is not updated.
I propose you to Create your dumb component with your template code above, but give it the searchText via an #Input(), and call your dumb component in your smart component
Smart component
<my-dumb-component [searchText]="searchText$ | async"></my-dumb-component>
Dumb component
#Input() searchText: SearchText
template
<div *ngIf="searchText">
results for "<b>{{searchText}}</b>"
</div>
This is because Angular is updates DOM interpolations before ngAfterViewInit and ngAfterViewChecked. I know this sounds confusing a bit. It's because of the first change detection cycle Angular does. Referring to Max Koretskyi's article about change detection algorithm of Angular, in a change detection cycle these happens sequentially:
sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
checks and updates input properties on a child component/directive
instance
updates child view change detection state (part of change detection
strategy implementation)
runs change detection for the embedded views (repeats the steps in
the list)
calls OnChanges lifecycle hook on a child component if bindings
changed
calls OnInit and ngDoCheck on a child component (OnInit is called
only during first check)
updates ContentChildren query list on a child view component
instance
calls AfterContentInit and AfterContentChecked lifecycle hooks on
child component instance (AfterContentInit is called only during
first check)
updates DOM interpolations for the current view if properties on
current view component instance changed
runs change detection for a child view (repeats the steps in this
list)
updates ViewChildren query list on the current view component
instance
calls AfterViewInit and AfterViewChecked lifecycle hooks on child
component instance (AfterViewInit is called only during first
check)
disables checks for the current view (part of change detection
strategy implementation)
As you see, Angular updates DOM interpolations (at step 9) after AfterContentInit and AfterContentChecked hooks are called, so if you call rxjs subscriptions in AfterContentInit or AfterContentChecked lifecycle hooks (or earlier, like OnInit etc.) your DOM will be updated because Angular updates DOM at step 10, and when you change something in ngAfterViewInit() and you are using OnPush, Angular won't update DOM because you are at step 12 on ngAfterViewInit() and Angular has already updated DOM before you change something!
There are workaround solutions to avoid this to subscribe it in ngAfterViewInit. First, you can call markForCheck() function, so you basically say by using it on the first cycle that "hey Angular, you updated DOM on step 9, but I have something to change at step 12, so please be careful, have a look at ngAfterViewInit I have still something to change". Or as a second solution, you can trigger a change detection manually again (by triggering and event handler or using detecthanges() function of ChangeDetectorRef) so that Angular repeats all these steps again, and when it reaches at step 9 again, Angular updates your DOM.
I have created a Stackblitz example that you can try these out. You can uncomment the lines of subscriptions placed in lifecycle hooks 1 by 1, so that you can see after which lifecycle hook Angular updates DOM. Or you can try triggering an event or triggering change detection cycle manually and see that Angular updates DOM on the next cycle.

Prevent ngOnChanges from firing after emitting event (Angular 2+)

In Angular 2+, custom two-way databinding can be accomplished by using #Input and #Output parameters. So if I want a child component to communicate with a third party plugin, I could do it as follows:
export class TestComponent implements OnInit, OnChanges {
#Input() value: number;
#Output() valueChange = new EventEmitter<number>();
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
// Update the third party plugin with the new value from the parent component
thirdPartyPlugin.setValue(this.value);
}
}
And use it like this:
<test-component [(value)]="value"></test-component>
After the third party plugin fires an event to notify us of a change, the child component updates the parent component by calling this.valueChange.emit(newValue). The issue is that ngOnChanges then fires in the child component because the parent component's value has changed, which causes thirdPartyPlugin.setValue(this.value) to be called. But the plugin is already in the correct state, so this is a potentially unnecessary/expensive re-render.
So what I often do is create a flag property in my child component:
export class TestComponent implements OnInit, OnChanges {
ignoreModelChange = false;
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
// Set ignoreModelChange to true if ngChanges will fire, so that we avoid an
// unnecessary (and potentially expensive) re-render.
if (this.value === newValue) {
return;
}
ignoreModelChange = true;
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
if (ignoreModelChange) {
ignoreModelChange = false;
return;
}
thirdPartyPlugin.setValue(this.value);
}
}
But this feels like a hack.
In Angular 1, directives which took in a parameter using the = binding had the same exact issue. So instead, I would accomplish custom two-way databinding by requiring ngModelController, which did not cause a re-render after a model update:
// Update the parent model with the new value from the third party plugin. After the model
// is updated, $render will not fire, so we don't have to worry about a re-render.
thirdPartyPlugin.onSomeEvent(function (newValue) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(newValue);
});
});
// Update the third party plugin with the new value from the parent model. This will only
// fire if the parent scope changed the model (not when we call $setViewValue).
ngModelCtrl.$render = function () {
thirdPartyPlugin.setValue(ngModelCtrl.$viewValue);
};
This worked, but ngModelController really seems to be designed for form elements (it has built in validation, etc.). So it felt a bit odd to use it in custom directives which are not form elements.
Question: Is there a best practice in Angular 2+ for implementing custom two-way databinding in a child component, which does not trigger ngOnChanges in the child component after updating the parent component using EventEmitter? Or should I integrate with ngModel just as I did in Angular 1, even if my child component is not a form element?
Thanks in advance!
Update: I checked out Everything you need to know about change detection in Angular suggested by #Maximus in the comments. It looks like the detach method on ChangeDetectorRef will prevent any bindings in the template from being updated, which could help with performance if that's your situation. But it does not prevent ngOnChanges from being called:
thirdPartyPlugin.onSomeEvent(newValue => {
// ngOnChanges will still fire after calling emit
this.changeDetectorRef.detach();
this.valueChange.emit(newValue);
});
So far I haven't found a way to accomplish this using Angular's change detection (but I learned a lot in the process!).
I ended up trying this with ngModel and ControlValueAccessor. This seems to accomplish what I need since it behaves as ngModelController in Angular 1:
export class TestComponentUsingNgModel implements ControlValueAccessor, OnInit {
value: number;
// Angular will pass us this function to invoke when we change the model
onChange = (fn: any) => { };
ngOnInit() {
thirdPartyPlugin.onSomeEvent(newValue => {
this.value = newValue;
// Tell Angular to update the parent component with the new value from the third
// party plugin
this.onChange(newValue);
});
}
// Update the third party plugin with the new value from the parent component. This
// will only fire if the parent component changed the model (not when we call
// this.onChange).
writeValue(newValue: number) {
this.value = newValue;
thirdPartyPlugin.setValue(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
}
And use it like this:
<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>
But again, if the custom component is not a form element, using ngModel seems a bit odd.
Also ran into this problem (or at least something very similar).
I ended up using hacky approach you discussed above but with a minor modification, I used setTimeout in order to reset state just in case.
(For me personally ngOnChanges was mainly problematic if using two-way binding, so the setTimeout prevents a hanging disableOnChanges if NOT using two-way binding).
changePage(newPage: number) {
this.page = newPage;
updateOtherUiVars();
this.disableOnChanges = true;
this.pageChange.emit(this.page);
setTimeout(() => this.disableOnChanges = false, 0);
}
ngOnChanges(changes: any) {
if (this.disableOnChanges) {
this.disableOnChanges = false;
return;
}
updateOtherUiVars();
}
This is exactly the intention of Angular and its something you should try to work with rather than against. Change detection works by components detecting changes in its template bindings and propagating them down the component tree. If you can design your application in such a way that you are relying on the immutability of components inputs', you can control this manually by setting #Component({changeDetection:ChangeDetectionStrategy.OnPush}) which will test references to determine whether to continue change detection on children components.
So, that said, my experience is that wrappers of 3rd party plugins may not efficiently handle and take advantage of this type of strategy appropriately. You should attempt to use knowledge of the above, together with good design choices like the separation of concerns of presentation vs container components to leverage the detection strategy to achieve good performance.
You can also pass changes: SimpleChanges to ngOnInit(changes: SimpleChanges) and inspect the object to learn more about your data flow.

Custom function for onPush change detection in Angular 2

{ id:123, version 1, ...}
I am writing an Angular 2 component that uses the onPush change detection strategy. I'd like to know if there is a way to use custom logic with onPush when determining if an object has changed. My understanding is that onPush checks the object reference an so only updates when a new instance of an object is sent.
My app tracks its own versioning for objects which means that every object has an id and version number. I periodically refresh data from the server which means that a new object is instantiated even though the data has not actually changed. This means there are cases with the default onPush that would see a new instantiated object and think it has to update. In actuality the id and version number are the same so even though the the object reference has changed I can skip updating the component.
I'd like to be able to provide a custom function to onPush that essentially uses a function like this to do change checking.
(obj1, obj2) => obj1.id === obj2.id && obj1.version === obj2.version
Is it possible to customize the onPush logic, or is there another way to achieve this so that I don't need to unnecessarily update the component when the object reference changes but the data does not.
I don't think that it's possible to create a custom changeDetectionStrategy . But if you do track the change on your objects by yourself you can do something (probably) even more optimized.
You can inject the ChangeDetectorRef and tell Angular that your component shouldn't be watched anymore (which is pretty cool and powerful !).
constructor(private _cd: ChangeDetectorRef)
ngOnInit() {
this._cd.detach();
}
ngOnChanges() {
// check if your objects have changed here
// if they've changed, tells Angular to check
// the bindings manually
this._cd.detectChanges();
}

Why does component view update when change detection is set to onPush? [duplicate]

I thought I was pretty clear on how Angular Change detection works after this discussion: Why is change detection not happening here when [value] changed?
But take a look at this plunk: https://plnkr.co/edit/jb2k7U3TfV7qX2x1fV4X?p=preview
#Component({
selector: 'simple',
template: `
<div (click)="onClick()">
{{myData[0].name}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Simple {
public #Input() myData;
constructor() {
}
public onClick() {
}
}
Click on a, it's changed to c
I understand that the click event triggers change detection on the App level, but [myData]="testData" is still referring to the same object, and I am using On Push on Simple, why does a get changed?
That's by design.
If you have component with OnPush change detection then its detectChangesInternal function won't be triggered unless one of four things happens:
1) one of its #Inputs changes
~2.4.x
~4.x.x
Note: #Inputs should be presented in template. See issue https://github.com/angular/angular/issues/20611 and comment
2) a bound event is triggered from the component (that is your case)
Caveats: There is some difference here between 2.x.x and 4
Angular ChangeDetectionStrategy.OnPush with child component emitting an event
~2.4.x
~4.x.x
3) you manually mark the component to be checked (ChangeDetectorRef.markForCheck())
4) async pipe calls ChangeDetectorRef.markForCheck() internally
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
https://github.com/angular/angular/blob/2.4.8/modules/%40angular/common/src/pipes/async_pipe.ts#L137
In other words if you set OnPush for component then after the first checking component's status will be changed from CheckOnce to Checked and after that it's waiting as long as we do not change status. It will happen in one of three things above.
See also:
https://github.com/angular/angular/issues/11678#issuecomment-247894782
There are also good explanations of how angular2 change detection work:
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
Here is Live Example(Thanks to Paskal) that explains onPush change detection. (Comp16 looks like your component. You can click at this box).

Angular2, doe's EventEmitter require zone.run?

I'm using ionic2, I implemented a class:
import {EventEmitter, Injectable} from 'angular2/core';
#Injectable()
export class LocalPushClear extends EventEmitter<number> {
constructor() {
super();
}
}
The class is used by on of my components to connect cordova plugin event to another component which subscribe to LocalPushClear, I listen to clear events, ones it fires, I emit using LocalPushClear and some other component subscribes:
this._LocalPushClear.subscribe(data => {
// Some action is taken here
});
The thing is that, I was expecting automatic change detection to be executed upon subscription callback execution(when its done), but it seems like there is no change detection execution at all, I have to do something like click a button or wrap my Some action with zone.run, I'm not sure if its a valid behavior or maybe I'm doing something wrong.
Edit:
I traces the code and it leads to Subject, so its basically custom event emitter that angular NgZone don't know about(at least I think), but I'm sure, if anyone could confirm, maybe future explain I will be very thankful.
You definitely should not extend EventEmitter. EventEmitter is only supposed to be used for #Output()s. Just use a Subject instead.
Angular doesn't get notified about values emitted by EventEmitter (when used this way) or Subject. Normally the code that causes the Observable (Subject) to emit new values is executed by code that causes change detection when completed for example when called from an event handler or setTimeout.
In your case the cause seems to be that the code that emits new values using LocalPushClear runs outside Angulars zone.
You can use one of the methods explained in https://stackoverflow.com/a/34829089/217408 to trigger change detection after the Observable emits an event.

Categories

Resources