Angular: RequestAnimationFrame and ChangeDetectorRef for updating view - javascript

In Angular, after a view has initialized, one part of it might be recalculated dynamically; in particular:
When someStream$ updates
I read the value of an already rendered element in the DOM
And set the value of another element (its height) according to the read value
an attribute listener in the template sets the height accordingly to the viewHeightvalue
So the code looks like this:
this._subs.push(this.someStream$.subscribe(() => {
this.zone.runOutsideAngular(() => {
requestAnimationFrame(() => {
const spaceTop: number = this.hostEl.nativeElement.getBoundingClientRect().top;
this.viewHeight = window.innerHeight - spaceTop;
this.cdr.detectChanges();
});
});
}));
<someElement [style.height.px]="viewHeight"></someElement>
I need ChangeDetectorRef.detectChanges(); to apply the changes instantly. My question is, why do I also need requestAnimationFrame? Why does the code not work without it? From my understanding requestAnimatoinFrame triggers re-rendering (in a browser-optimized way), but NgZone, activated by ChangeDetectorRef, should do that already.

Related

Angular Template Interpolation Slow to Respond

My Angular template needs to display a rapidly changing value, driven by mousemove events, which I am retrieving from my NgRx Store. The Store appears to be keeping up with the data changes but the resulting value displayed in the template lags behind and appears to only refresh when the mouse stops moving.
A component with approximately 300 DOM elements detects mousemove events and handles them outside of ngZone as follows:
ngAfterViewInit() {
const eventElement = this.eventDiv.nativeElement;
this.move$ = fromEvent(eventElement, 'mousemove');
this.leave$ = fromEvent(eventElement, 'mouseleave');
/*
* We are going to detect mouse move events outside of
* Angular's Zone to prevent Change Detection every time
* a mouse move event is fired.
*/
this.ngZone.runOutsideAngular(() => {
// Check we have a move$ and leave$ objects.
if (this.move$ && this.leave$) {
// Configure moveSubscription.
this.moveSubscription = this.move$.pipe(
takeUntil(this.leave$),
repeat()).subscribe((e: MouseEvent) => {
e.stopPropagation();
this.mouseMove.emit(e);
});
};
});
A parent component handles the resulting mouseMove event and still outside ngZone performs some Calculations to ascertain which element the mouse is over. Once the result has been calculated a function is called, passing in the calculated result, and within this function I dispatch an NgRx Action within ngZone using this.ngZone.run(() => { dispatch Action here }.
I can see that the Store reacts quickly to the changing data. I then have a separate component responsible for displaying the result. An Observable listens to the Selector's changing values and displays the result using interpolation.
Curiously I added an RxJs tap into the Observable declaration as follows:
public mouseoverLocationName$: Observable<string | null>;
constructor(
public store: Store<fromPilecapReducers.PilecapState>
) {
this.mouseoverLocationName$ = this.store.pipe(
select(fromPilecapSelectors.PilecapMapSelectors.selectMouseoverLocationName),
tap(locationName => {
console.log(`mouseoverLocation$: ${locationName}`);
})
);
}
The console logs out the locationName value nice and quickly. However the html element displaying the string is very slow and, as I said earlier, only appears to update when the mouse stops moving. The template code is as follows:
<h2>{{mouseoverLocationName$ | async}}</h2>
I've got to the point now where I can't see the wood for the trees! any suggestions or guidance very welcome.

How to get access to nativeElements on ngOnInit?

Suppose in my angular code i have access to a html element by viewChild('someDiv') or constructor(private elem: ElementRef){}.Whenever my angular component gets loaded i want access to that elements some property so i can store it in variable.Ex- i want to get the elements width as soon as that component gets loaded so my code is
#ViewChild('someDiv') someDiv: ElementRef;
someDivWidth;
ngOnInit(){
someDivWidth = this.someDiv.nativeElement.clientWidth;
}
and my html code
<div #someDiv>some text</div>
But this is giving error like nativeElement is undefined.So how can i solve this?My basic need is as soon as this component loads i want to set that div's width in variable without triggering
any function.
Update:
So using constructor(private elem: ElementRef){} isn't giving any error and i can get access to width by
ngOnInit(){
this.someDivWidth = this.elem.nativeElement.querySelector('.someCssSelector');
}
But suppose i have some products data which i am fetching from backend and storing in an array in ngOnInit lifecycle hook and in my template creating multiple div's depending upon the products data array using lifecycle hook.In that case if i want to get all the product div's currently i am doing like
products = [];
items;
ngOnInit(){
this.productService.getProducts().subscribe(products => {
this.products = products
})
this.itmes = this.elem.nativeElement.querySelectorAll('.each-product');
console.log(this.items)
}
this is giving me a empty nodeList.I thought at the time of setting this.items my products don't gets loaded from backend thats why its giving me empty array or nodeList but putting that last two lines in subscribe() like
ngOnInit(){
this.productService.getProducts().subscribe(products => {
this.products = products
this.itmes = this.elem.nativeElement.querySelectorAll('.each-product');
console.log(this.items)
})
}
and my html code is
<div>
<div *ngFor='let product of products' class='each-product'>...</div>
</div>
is still giving me an empty array or nodelist.What to do??
You should do it in the ngAfterViewInit method of the angular life cycle.
ngAfterViewInit(){
this.someDivWidth = this.elem.nativeElement.querySelector('.someCssSelector');
}
Angular uses its algorithms to detect changes in data and respond to the view.If you want to opreate the DOM after the request was completed, you may not be able to get it because the view hasn't been updated yet, and you can understand that Angular also needs time to update the view.
Offer two solutions
Use ChangeDetectorRef to perform change detection manually
use setTimeout
stackblitz-demo
ngOnInit lifeCycle is invoked once when component instantiated and called right after the properties checked for the first time, and before its children checked.
If You want to be sure that view has been loaded (#ViewChild depends on view to be rendered) you should use ngAfterViewInit which is called after component view and it's children view are created and ready.
UPDATE: If you need to access viewChild in ngOnInit you have to set { static: true }

Changing ContentChildren models on QueryList.changes

Suppose I have a parent component with #ContentChildren(Child) children. Suppose that each Child has an index field within its component class. I'd like to keep these index fields up-to-date when the parent's children change, doing something as follows:
this.children.changes.subscribe(() => {
this.children.forEach((child, index) => {
child.index = index;
})
});
However, when I attempt to do this, I get an "ExpressionChangedAfter..." error, I guess due to the fact that this index update is occurring outside of a change cycle. Here's a stackblitz demonstrating this error: https://stackblitz.com/edit/angular-brjjrl.
How can I work around this? One obvious way is to simply bind the index in the template. A second obvious way is to just call detectChanges() for each child when you update its index. Suppose I can't do either of these approaches, is there another approach?
As stated, the error comes from the value changing after the change cycle has evaluated <div>{{index}}</div>.
More specifically, the view is using your local component variable index to assign 0... which is then changed as a new item is pushed to the array... your subscription sets the true index for the previous item only after, it has been created and added to the DOM with an index value of 0.
The setTimout or .pipe(delay(0)) (these are essentially the same thing) work because it keeps the change linked to the change cycle that this.model.push({}) occurred in... where without it, the change cycle is already complete, and the 0 from the previous cycle is changed on the new/next cycle when the button is clicked.
Set a duration of 500 ms to the setTimeout approach and you will see what it is truly doing.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
setTimeout(() => {
foo.index = index;
}, 500)
});
});
}
It does indeed allow the value to be set after the element is rendered on
the DOM while avoiding the error however, you will not have the value
available in the component during the constructor or ngOnInit if
you need it.
The following in FooComponent will always result in 0 with the setTimeout solution.
ngOnInit(){
console.log(this.index)
}
Passing the index as an input like below, will make the value
available during the constructor or ngOnInit of FooComponent
You mention not wanting to bind to the index in the template, but it unfortunately would be the only way to pass the index value prior to the element being rendered on the DOM with a default value of 0 in your example.
You can accept an input for the index inside of the FooComponent
export class FooComponent {
// index: number = 0;
#Input('index') _index:number;
Then pass the index from your loop to the input
<foo *ngFor="let foo of model; let i = index" [index]="i"></foo>
Then use the input in the view
selector: 'foo',
template: `<div>{{_index}}</div>`,
This would allow you to manage the index at the app.component level via the *ngFor, and pass it into the new element on the DOM as it is rendered... essentially avoiding the need to assign the index to the component variable, and also ensuring the true index is provided when the change cycle needs it, at the time of render / class initialization.
Stackblitz
https://stackblitz.com/edit/angular-ozfpsr?embed=1&file=src/app/app.component.html
One way is update the index value using a Macro-Task. This is essentially a setTimeout, but bear with me.
This makes your subscription from your StackBlitz look like this:
ngAfterContentInit() {
this.foos.changes.subscribe(() => {
// Macro-Task
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
}, 0);
});
}
Here is a working StackBlitz.
So the javascript event loop is coming into play. The reason for the "ExpressionChangedAfter..." error is highlighting the fact that changes are being made to other components which essentially mean that another cycle of change detection should run otherwise you can get inconsistent results in the UI. That's something to avoid.
What this boils down to is that if we want to update something, but we know it shouldn't cause other side-effects, we can schedule something in the Macro-Task queue. When the change detection process is finished, only then will the next task in the queue be executed.
Resources
The whole event loop is there in javascript because there is only a single-thread to play with, so it's useful to be aware of what's going on.
This article from Always Be Coding explains the Javascript Event Loop much better, and goes into the details of the micro/macro queues.
For a bit more depth and running code samples, I found the post from Jake Archibald very good: Tasks, microtasks, queues and schedules
The problem here is that you are changing something after the view generation process is further modifying the data it is trying to display in the first place. The ideal place to change would be in the life-cycle hook before the view is displayed, but another issue arises here i.e., this.foos is undefined when these hooks are called as QueryList is only populated before ngAfterContentInit.
Unfortunately, there aren't many options left at this point. #matt-tester detailed explanation of micro/macro task is a very helpful resource to understand why the hacky setTimeout works.
But the solution to an Observable is using more observables/operators (pun intended), so piping a delay operator is a cleaner version in my opinion, as setTimeout is encapsulated within it.
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
}
here is the working version
use below code, to make that changes in the next cycle
this.foos.changes.subscribe(() => {
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
});
I really don't know the kind of application, but to avoid playing with ordered indexes , it is often a good idea to use uid's as index.
Like this, there is no need to renumber indexes when you add or remove components since they are unique.
You maintain only a list of uids in the parent.
another solution that may solve your problem , by dynamically creating your components and thus maintain a list of these childs components in the parent .
regarding the example you provided on stackblitz (https://stackblitz.com/edit/angular-bxrn1e) , it can be easily solved without monitoring changes :
replace with the following code :
app.component.html
<hello [(model)]="model">
<foo *ngFor="let foo of model;let i=index" [index]="i"></foo>
</hello>
hello.component.ts
remove changes monitoring
added foocomponent index parameter
import { ContentChildren, ChangeDetectorRef, Component, Input, Host, Inject, forwardRef, QueryList } from '#angular/core';
#Component({
selector: 'foo',
template: `<div>{{index}}</div>`,
})
export class FooComponent {
#Input() index: number = 0;
constructor(#Host() #Inject(forwardRef(()=>HelloComponent)) private hello) {}
getIndex() {
if (this.hello.foos) {
return this.hello.foos.toArray().indexOf(this);
}
return -1;
}
}
#Component({
selector: 'hello',
template: `<ng-content></ng-content>
<button (click)="addModel()">add model</button>`,
})
export class HelloComponent {
#Input() model = [];
#ContentChildren(FooComponent) foos: QueryList<FooComponent>;
constructor(private cdr: ChangeDetectorRef) {}
addModel() {
this.model.push({});
}
}
I forked this working implementation : https://stackblitz.com/edit/angular-uwad8c

The React way to retrieve (some) DOM values

My react Menu component renders a list of links. Other parts need to animate (width and x/y pos) depending on the width of the Menu component's link's widths so all this needs to happen after all has rendered, and I need to read values from the DOM.
I'm looking for a proper way, no react anti-pattern, to handle this scenario but can't really sort it out in a way I'm happy with.
Here's an explanatory flow.
1) User clicks a link in the menu.
2) Activate that link.
this.setState({link: {active: true}});
3) Look up and store what width the clicked element has.
let w = this.myLink.offsetWidth; // myLink from refs.
4) Update state with new width
this.setState({styleState: { width: w }}); // Styles used in component elsewhere.
OK! This works fairly well, now I want to do the same for pos X/Y which makes things more complicated. Now I'll need to getBoundingClientRect() and take consideration to this element's parents to get the correct position etc.
I keep wishing for a sensible DOM API where I can grab values from the DOM (yep, jQuery style - like jqLite), then with values I update my state and thus re-render the component. I realize it wouldn't make sense to manipulate the DOM this way so I'm not trying to, but in this specific case the truth is actually in the DOM already and I need to extract it.
Is there a lib ppl use for these things, just to get a couple of getter methods like el.width(), el.left() for convenience?
I don't think its bad style to read size/styles of the dom after rendering. Just be sure to not modify the dom directly. The reason is that the real dom should always be in sync with the virtual dom.
I would encapsulate this behaviour in a custom Link component that receives an onClick:
//untested pseudocode
class Link extends Component {
handleRendered = (el) => {
this.width = el.clientWidth;
....
}
handleClick = (ev) => {
this.props.onClick({
width: this.width,
x: this.x,
...
})
}
render = () => {
return (
<div ref={(el) => this.handleRendered(el)} onClick={this.handleClick}>
{this.props.children}
</div>
);
}
}
class MyPage extends Component {
handleClick = ({width, ....}) => {
this.setState({
otherElementStyle: {
width: width
}
})
}
render = () => {
return (
<Link onClick={this.handleClick}>
Menu1
</Link>
);
}
}
Concerning your requested library, I'm not aware of any but I bet there is one.

Incrementing the Material Design Lite Progress bar with React

I've got MDL running with React at the moment and it seems to be working fine at the moment.
I've got the Progress Bar appearing on the page as needed and it loads up with the specified 'progress' on page load when either entering in a number directly:
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(10);
})
or when passing in a number via a Variable:
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(value);
})
It stops working after this though. I try to update the value via the Variable and it doesn't update. I've been advised to use this:
document.querySelector('.mdl-js-progress').MaterialProgress.setProgress(45);
to update the value but it doesn't work. Even when trying it directly in the console.
When trying via the Console I get the following Error:
Uncaught TypeError: document.querySelector(...).MaterialProgress.setProgress is not a function(…)
When I try to increment the value via the Variable I get no errors and when I console.log(value) I am presented the correct number (1,2,3,4...) after each click event that fires the function (it fires when an answer is chosen in a questionnaire)
What I want to know is if there's something obvious that I'm missing when using MTL and React to make components to work? There was an issue with scope but I seem to have it fixed with the following:
updateProgressBar: function(value) {
// fixes scope in side function below
var _this = this;
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(value);
})
},
In React I've got the parent feeding the child with the data via props and I'm using "componentWillReceiveProps" to call the function that updates the progress bar.
I've used the "componentDidMount" function too to see if it makes a difference but it still only works on page load. From what I've read, it seems that I should be using "componentWillReceiveProps" over "componentDidMount".
It's being fed from the parent due to components sending data between each other. I've used their doc's and some internet help to correctly update the parent function to then update the progress bar in the separate component.
updateProgressBarTotal: function(questionsAnsweredTotal) {
this.props.updateProgressBarValue(questionsAnsweredTotal);
}
The parent function looks like the following (I think this may be the culprit):
// this is passed down to the Questions component
updateProgressBarTotal: function(questionsAnsweredTotal) {
this.setState({
progressBarQuestionsAnswered : questionsAnsweredTotal
})
}
I can post up some more of the code if needed.
Thank you
Looks I needed a fresh set of eyes on this.
I moved the function to the child of the parent. It seems that using document.querySelector... when in the parent doesn't find the element but when it's moved to the child where I do all the question logic it seems to be fine. It increments the progress correctly etc now :)
// goes to Questionnaire.jsx (parent) to update the props
updateProgressBarTotal: function(questionsAnsweredTotal) {
// updates the state in the parent props
this.props.updateProgressBarValue(questionsAnsweredTotal);
// update the progress bar with Value from questionsAnsweredTotal
document.querySelector('.mdl-js-progress').MaterialProgress.setProgress(questionsAnsweredTotal);
},
I had same problem in angular2 application.
You don't necessary need to move to the child component.
I found after struggling to find a reasonable fix that you simply have to be sure mdl-componentupgradedevent already occurred before being able to use MaterialProgress.setProgress(VALUE). Then it can be updated with dynamic value.
That is why moving to the child works. In the parent component mdl-componentupgraded event had time to occur before you update progress value
My solution for angular2 in this article
Adapted in a React JS application :
in componentDidMount, place a flag mdlProgressInitDone (initiated to false) in mdl-componentupgraded callback :
// this.ProgBar/nativeElement
// is angular2 = document.querySelector('.mdl-js-progress')
var self = this;
this.ProgBar.nativeElement.addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(0);
self.mdlProgressInitDone = true; //flag to keep in state for exemple
});
Then in componentWillReceiveProps test the flag before trying to update progress value :
this.mdlProgressInitDone ? this.updateProgress() : false;
updateProgress() {
this.ProgBar.nativeElement.MaterialProgress.setProgress(this.currentProgress);
}
After attaching the progress bar to the document, execute:
function updateProgress(id) {
var e = document.querySelector(id);
componentHandler.upgradeElement(e);
e.MaterialProgress.setProgress(10);
}
updateProgress('#questionnaireProgressBar');

Categories

Resources