How to prevent change detection on scroll? ( Angular ) - javascript

I have a large component, and I would like to avoid unnecessary change detections to increase performance.
I added changeDetection: ChangeDetectionStrategy.OnPush to the #Component.
I would like to update the style of an element if it is scrolled. Unfortunately Angular runs the change detection, every time I scroll that element.
I tried to add an event listener to the native element to avoid this, but the change detection is still running again when I scroll:
#ViewChild('my_element') my_element_ref: ElementRef;
ngOnInit() {
this.my_element_ref.nativeElement.addEventListener('scroll', this.updateStyle);
}
ngAfterViewChecked() {
console.log('check');
}
The ngAfterViewChecked is called even if this.updateStyle is an empty function.
But if I comment out this.my_element_ref.nativeElement.addEventListener('scroll', this.onScroll), then ngAfterViewChecked is not called anymore.
How is it possible to call a function when an element is scrolled, but avoid Angular's change detection?

I would suggest you use ngZone.runOutsideAngular.
constructor (private zone : NgZone) {}
ngAfterViewInit () : any {
this.zone.runOutsideAngular(() => {
window.addEventListener('scroll', (e)=> {
console.log( 'scroll event fired' );
});
});
}

You might want to look at ChangeDetectorRef API.
Specifically you would detach the change detection for the component using detach() (maybe in the constructor like below) and then mark component for change in your scroll function via markForCheck().
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
this.cd.markForCheck();
Below links for your reference.
https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
https://angular.io/api/core/ChangeDetectorRef

just inject NgZone in your constructor and then subscribe to an event like this:
this.ngZone.runOutsideAngular(() => {
....addEventListener('...);
});
keep in mind that change detection will not be called in this case, so you won't be able to change styles with template binding. It should be changed by native javascript or using Renderer2

Related

Using getters in Angular Component Templates?

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.

RxJS: Mimic Angulars #Input() binding?

Im pretty new to Angular 8 and RxJS and stumbled upon an issue:
I make an angular app which relies heavily on an externally loaded THREE.js scene. Services handle those scenes.
So most of the time there is No HTML Template (only maintaining scene via js object) => no bindings ?
So I was thinking... is there a way to use Rxjs subjects/observables to achieve something like Input() binding?
thats what i basically want to
this.sub = myService.watch('window.deviceorientation')
.subscribe({next(x => { if(x) this.sub.unsubscribe; doStuff();})})
I want to continously check a certain object (any really), be notified as soon as it exists (or instantly if it already does). I bet there is some weird combination of RxJS Operators which can do exactly this?
(So basically its a little bit like AngularJS scope.$watch but to keep performance I'd of course clean up subscriptions.)
You can use the fromEvent rxjs creation utility to achieve the desired effect.
import { fromEvent } from 'rxjs';
orientation$ = fromEvent(window, 'deviceorientation');
const subscription = orientation$.subscribe((event)=>{
//do stuff
});
// when disponse is required
subscription.unsubscribe();
So I worked my way around this.
I created a root-level component that allows me to watch for specific changes on scope:
//HTML
<app-change-detector [watch]="window.anyObject" (onChange)="anyRootService.onObjectChanged($event)">
//TS
#Component({
selector: 'app-change-detector'
})
export class ChangeDetectorComponent implements OnChanges {
#Input() watch: any;
#Output() onChange = new EventEmitter<any>();
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.watch) {
this.onChange.emit(changes.watch.currentValue);
}
}
}

Reloading DOMContent

I understand angular is one page application with multiple components and use route to interact between pages.
We have an angular app like that. One of the requirements is that on a specific component we need to add an eventListener to DomContentLoaded event.
Since the index.html page has been loaded (hence DomContentLoaded event has been fired) way before, we have a problem.
I cannot find a way to re-trigger the DomContentLoaded event.
The DomContentLoaded event fired before the first component was created. You can however use the ngAfterViewInit() method.
ngAfterViewInit() is a callback method that is invoked immediately after Angular has completed initialization of a component's view. It is invoked only once when the view is instantiated.
class YourComponent {
ngAfterViewInit() {
// Your code here
}
}
If you really need to catch the DomContentLoaded event anyway, do it in the main.ts file.
I don't know exactly, why you need to listen to DomContentLoaded in angular, because there are plenty of functions there, depends on which Version of angular do you use.
export class SomeComponent implements AfterViewInit {
ngAfterViewInit(): void {
}
}
But, you can retrigger an rendering of an component with *ngIf (Angular2+) or ng-if (AngularJS). Just put a boolean there and switch the boolean on an event.
Please try not use such listeners, they are (often) not needed in Angular. Try to use Component lifecycle hooks.
In your case, I recommend using ngAfterViewChecked(), which fires when the component finished rendering.
import { Component, AfterViewChecked } from '#angular/core';
#Component({
...
})
export class WidgetLgFunnelsComponent implements AfterViewChecked {
ngAfterViewChecked() {
console.log('Component finished rendering!')
}
}
If you want to do something with the DOM elements inside the template of the component, and you want to make sure they are there, you should use the ngAfterViewInit hook from angular:
#Component({
// ...
})
export class SomeComponent implements AfterViewInit {
ngAfterViewInit(): void {
// here you can be sure that the template is loaded, and the DOM elements are available
}
}

Call method after view (DOM) is updated (in Angular 2)

Assume that I have the following directive:
import {Directive, Input, OnChanges } from '#angular/core'
#Directive({
selector: 'select[my-select]'
})
export class NbSelect implements OnChanges {
#Input() ngModel;
constructor() {
}
ngOnChanges(changes) {
doSomeStuffAfterViewIsUpdated();
}
doSomeStuffAfterViewIsUpdated() {
console.log(jQuery('select[my-select]').val()); // this should write the new value
}
}
I'm using it in somewhere in a template: ...<select my-select [(ngModel)]="someReference">...
At some point I change the value of someReference in a component: someReference = "newValue"
What is happening now: The doSomeStuffAfterViewIsUpdated function is called before the DOM is updated. Which means the console shows the old value of the select.
What I want: The doSomeStuffAfterViewIsUpdated function should be called after the DOM is updated by angular, so the console shows the "newValue".
What should I use instead of ngOnChanges to make it work like I want?
Please note that the important thing here is that the code should run after the DOM update. The question is not about how can I access the new value of the element.
Thx!
I'm a bit late to the party, but since I had a similar problem and a probable solution, I thought I'd share:
What you're looking for is MutationObservers (CanIUse);
I solved a very similar issue with a directive similar to this (simplified for... simplicity? :P):
ngOnChanges() {
this.observer = new MutationObserver(() => { this.methodToCall() });
this.observer.observe(this.elRef.nativeElement, this.observerOptions); // See linked MDN doc for options
}
private methodToCall(): void {
this.observer.disconnect();
}
ngOnDestroy() {
if(this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
The MutationObserver callback will be triggered when either an attribute, children, etc is modified in the DOM (or added, deleted, etc)
It is possible you won't need the ngOnChanges at all depending on your use case and it would make the code a bit cleaner too.

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

Categories

Resources