Execute function after DOM has finished rendering - javascript

I recall reading the excerpt below from a blog.
$timeout adds a new event to the browser event queue (the rendering engine is already in this queue) so it will complete the execution before the new timeout event.
I'm wondering if there is a better way in angular/ javascript than using
setTimeout(() => {
// do something after dom finishes rendering
}, 0);
to execute code when the DOM has completely finished a task such as updating an *ngFor and rendering the results on the page.

You might try the ngAfterViewInit life-cycle hook, which is the chronologically last single-fire life-cycle hook.
https://angular.io/guide/lifecycle-hooks
It works much like ngInit but it fires after the view and child views have initialized.
If you need something that fires every time the DOM finishes you can try ngAfterViewChecked or ngAfterContentChecked.

problem:
I need to run a function sometimes after some parts loaded. (I wanted to stretch out an input and a label)
ngAfterViewInit and route change detection didn't solve my problem
Solution:
I made a component which
import { Component, AfterViewInit } from '#angular/core';
declare var jquery: any;
declare var $: any;
#Component({
selector: 'app-inline-label',
templateUrl: './inline-label.component.html',
styleUrls: ['./inline-label.component.scss']
})
/** InlineLabel component*/
/**
this component stretch inline labels and its input size
*/
export class InlineLabelComponent implements AfterViewInit {
/** InlineLabel ctor */
constructor() {
}
ngAfterViewInit(): void {
var lblWidth = $('.label-inline').width();
var parentWidth = $('.label-inline').parent().width();
var fieldWidth = parentWidth - lblWidth;
$('.form-control-inline').css("width", fieldWidth);
}
}
then I used it anywhere in my html like
<app-inline-label></app-inline-label>
even if my html had *ngIf="", I used app-inline-label inside that tag and solved all my problems
Actually it will be fired exactly when <app-inline-label> </app-inline-label> being rendered

If the function to be rendered multiple times ngAfterContentChecked will be preferable.
app.component.ts
export class AppComponent implements OnInit, OnDestroy {
searchRegister: any = [];
constructor() {
}
ngAfterContentChecked(): void {
this.setHTMLElements();
}
setHTMLElements() {
this.searchRegister = ['cards-descriptor__subtitle','insights-card__title','article-headline__title','wysiwyg__content','footer-logo__heading','hero-breadcrumbs__blurb','multi-column-text__body','small-two-column-image-text__blurb','two-column-image-text__blurb','image-blurbs-expandable__desc',];
for (var val of this.searchRegister) {
var classLength = this.dom.body.getElementsByClassName(val).length;
for (var i = 0; i <= classLength; i++) {
if (
this.dom.body.getElementsByClassName(val)[i]?.innerHTML != undefined
) {
this.dom.body.getElementsByClassName(val)[
i
].innerHTML = this.dom.body
.getElementsByClassName(val)
[i]?.innerHTML?.replace(/[®]/gi, '<sup>®</sup>');
}
}
}
}
}
Other.component.ts
import { AppComponent } from '../../app.component';
export class IndustryComponent implements OnInit {
constructor(private appComponent: AppComponent) { }
ngAfterContentChecked(): void {
this.appComponent.setHTMLElements();
}
}

Related

Get innerHTML from component AFTER render

I am trying to "steal" from the DOM the SVG code generated by an own component. I do it like this:
<my-own-component id="my-component-id" #myComponentId
[data]="data"></my-own-component>
onButtonClick() {
this.data = someData;
const svgCode = document.getElementById('my-component-id').innerHTML;
}
Also tried (also not working):
#ViewChild('myComponentId') myComponentId;
...
onButtonClick() {
this.data = someData;
const svgCode = this.myComponentId.nativeElement.children[0].innerHTML;
}
The problem is that I get the content before Angular has applied the changes caused by this.data = someData, so the elements of the SVG are not included.
I have "solved" it introducing a 50ms timeout. This works, but is not a proper solution, is a bad patch:
this.data = someData;
await new Promise(resolve => setTimeout(resolve.bind(null, null), 50));
const svgCode = document.getElementById('my-component-id').innerHTML;
I would like to be able to wait for Angular to finish rendering the component. Is there any way to do it?
Thanks in advance.
Elezan, the problem is that you need "give a breath to Angular". If you has, e.g.
<div>{{data}}</div>
click(){
this.data="......."
//here you don't can check the value of innerHtml
}
This "breath" is use a setTimeout
<div>{{data}}</div>
click(){
this.data="......."
setTimeout(()=>{
//now here you can check the value of innerHtml
})
}
Think that Angular, when you call to click function, execute all the instructions and "repaint" the app. So in the first case you're trying to get the innerHTML before Angular "repaint". Using a setTimeout you're saying to Angular: "Hey! you repaint and, after, don't forget the instructions into setTimeout" -see that setTimeout has no milliseconds-
Another way is inject in constructor ChangeDetectorRef and use markForCheck() before try to get the innerHTML
Update Another example using observables
$observable.subscribe(res=>{
this.data=res
setTimeout(()=>{
..get the innerHTML
})
})
Or promise
$promise.then(
res=>{
this.data=res
setTimeout(()=>{
..get the innerHTML
}),
err=>{...}
)
Or await:
const svgCode = await new Promise<string>(resolve => {
setTimeout(() => {
resolve(document.getElementById('my-component-id').innerHTML));
});
});
Try AfterViewInit lifecycle hook. It's implementing like this
export class MyComponent implements AfterViewInit {
ngAfterViewInit() {
// code that should be executed after view initialization
}
}
You just need to add AfterViewInit lifecycle hook with the class. I would also suggest that you assign the property within the OnInit lifescycle as well. Your parent component should look like this
export class appComponent implements OnInit, AfterViewInit{
ngOnInit(): void {
this.data = someData;
}
ngAfterViewInit(): void{
const svgCode = document.getElementById('my-component-id').innerHTML;
}
}

Angular performance: change detection detached if component is out of viewport

I want detach the change detection for all the compontents out of the current viewport
see demo online
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, ElementRef, ViewChild, OnInit, OnDestroy, AfterViewInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<div #counter>[{{index}}] {{count}}</div>`,
styles: [`div { border: 1px solid red; padding: 5px 5px 5px 5px; }`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HelloComponent implements OnInit, AfterViewInit {
#ViewChild('counter', { static: false }) counter: ElementRef;
#Input() index: number;
public count = 0;
public visible = true;
constructor(private cdr: ChangeDetectorRef){}
ngOnInit() {
setInterval(() => {
this.count++;
this.cdr.markForCheck();
}, 1000);
}
ngAfterViewInit() {
const hideWhenBoxInView = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) { // If not in view
this.cdr.detach();
this.visible = false;
} else {
this.visible = true;
this.cdr.reattach();
this.cdr.markForCheck();
}
// console.log(this.index, this.visible);
});
hideWhenBoxInView.observe(this.counter.nativeElement);
}
}
it works, but with over 1000 components the performance is very bad.
Are my attaching/detaching change detection correct?
You are calling setInterval() for every component including those that are not in view. Change detection is not running but you are still calling the function in setInterval() 1000 times per second which explains the lag.
By the way, rendering a scroll list with 1000 items affects performance too. Browsers will render everything and need to calculate various paints when scrolling through the list despite being out of viewport. You should render such long list lazily, see Virtual Scrolling in Angular 7
You are also calling .markForCheck() on components that are out of view, check whether component is visible before calling that.
See StackBlitz
ngOnInit() {
this.subscriptions.add(
interval(1000).subscribe(() => {
this.count++;
if (this.visible) {
this.cdr.markForCheck();
}
})
);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
ngAfterViewInit() {
const hideWhenBoxInView = new IntersectionObserver(entries => {
if (entries[0].intersectionRatio <= 0) {
// If not in view
this.cdr.detach();
this.visible = false;
} else {
this.visible = true;
this.cdr.reattach();
this.cdr.markForCheck();
}
});
hideWhenBoxInView.observe(this.counter.nativeElement);
}
Maybe using trackBywill allow to avoid check whether it is in viewport.
<li *ngFor="let item of items; index as i; trackBy: trackByFn">...</li>
trackBy gives you a possibility to choose what property/condition angular should check changes against.
trackByFn(index, item) {
return item.someUniqueIdentifier;
// return index(if id is not unique) or unique id;
}
As Angular docs says:
A function that defines how to track changes for items in the
iterable.
When items are added, moved, or removed in the iterable, the directive
must re-render the appropriate DOM nodes. To minimize churn in the
DOM, only nodes that have changed are re-rendered.
By default, the change detector assumes that the object instance
identifies the node in the iterable. When this function is supplied,
the directive uses the result of calling this function to identify the
item node, rather than the identity of the object itself.
The function receives two inputs, the iteration index and the node
object ID.

Angular 4+ Function reloading each time the component loads

So here is the function
greenToAmber() {
let x = 0;
setInterval(function () {
x++;
..... Rest of code
}, 500);
}
}
I've set this component up using the routes as you would expect, I've tried calling the function in OnInit as well, but every time I go this component then off it and back again the counter will launch a second instance of the counter & then a third ect for each time I leave & come back to the page.
From my understanding I thought ngOnDestroy was meant to prevent this, I'm assuming that I'll need to subscribe and then unsubscribe to the function maybe on destroy?
But I'm relatively new to angular 4 so pretty inexperienced.
setInterval is not destroyed on component destroy, you have to save the interval id in your class and use clearInterval javascript native function to clean it on your component destroy hook ngOnDestroy:
import {Component, OnDestroy} from '#angular/core';
#Component({ ... })
export class YourClass implements OnDestroy {
public intervalId: any;
public greenToAmber() {
let x = 0;
// registering interval
this.intervalId = setInterval(function () {
// ..... Rest of code
}, 500);
}
}
public ngOnDestroy () {
if (this.intervalId !== undefined) {
clearInterval(this.intervalId); // cleaning interval
}
}
}
Hopes it helps.
You're setting a demon process with setInterval. The behavior you mention is expected. That is how JavaScript works. Its not Angular specific.
SetInterval always returns a ID which you might want to track in your controller. When you want to destroy it, make sure you do it specifically.
eg:
greenToAmber() {
let x = 0;
$scope.myDemon = setInterval(function () {
x++;
..... Rest of code
}, 500);
}
}
//Somewhere else; where you want to destroy the interval/stop the interval:
If($scope.myDemon) {
clearInterval($scope.myDemon);
}

Explanation for the .next() function in angular

import { Component, Input, Output, EventEmitter } from '#angular/core';
var colorPickerCss = "app/css/ui/color-picker.css";
var colorPickerTemplate = "app/partials/color-picker.html";
#Component({
selector: 'color-picker',
styleUrls: [colorPickerCss],
templateUrl: colorPickerTemplate
})
export class ColorPicker{
#Input() colors: string[] = [];
#Output() selectedColor = new EventEmitter();
isSelectorVisible : boolean = false;
showSelector(value: boolean){
this.isSelectorVisible = value;
}
selectColor(color: string){
this.showSelector(false);
this.selectedColor.next({color});
}
} ;
I have written the above code, but I want to understand the functioning of it. My question is, what is the .next() function on this line this.selectedColor.next({color}). What library is it from? I have mentioned the imports above, but I can't really get to the actual definition of this function.
An EventEmitter, extends Subject. When you use next, you fire off an event that all subscribers will listen too. See here. emit is the preferred alternative.

Promise for *ngFor in Angular2

i'm using angular2 and i'm trying to get the .style.width attribute from an element which is loaded inside a *ngFor- loop. I want to use it to define the width of some other elements. This should be done while the page loads. I dont want to store the width as some var inside my code. I want to get it directly from the dom.
promise :
let kistenPromise = new Promise(function(resolve, reject){
let itemListContainer = document.getElementById("itemContainer");
if(itemListContainer.children[0] != undefined){
resolve(itemListContainer.children);
}else{
reject(Error("Promise was not fullfilled!"));
}
}.bind(this));
handler:
kistenPromise.then(
function(result){
console.log(result);
}.bind(this), function(err){
console.log(err);
}.bind(this));
html:
<div class="itemFrame" id="itemContainer">
<div class="listStyle" *ngFor="let item of list">{{item}}</div>
</div>
When i use the colde like this it only returns the Promise was not fullfilled.
However if i try itemList.children != undefined and return the .length it will return 0.What am i missing?
Thanks in advance!
You may want to use AfterViewInit. Add a local variable #itemContainer to your container:
<div class="itemFrame" id="itemContainer" #itemContainer>
<div class="listStyle" *ngFor="let item of list">{{item}}</div>
</div>
Then in your component you can check the element for children (or pass the element to another function that checks for it):
import { ViewChild, ElementRef, AfterViewInit } from '#angular/core';
export class YourComponent implements AfterViewInit {
#ViewChild('itemContainer') itemContainer: ElementRef;
ngAfterViewInit() {
if (this.itemContainer.nativeElement.children.length) {
// ...
} else {
// ...
}
}
}
getElementById returns a single element, because id should be unique, and not an array. Other queries do return and array (for example getElementsByClassName or getElementsByTagName).
Where is your handler written? It may be getting executed before the view is even initialized. If so, try moving it to ngAfterViewInit
your.component.ts
import { AfterViewInit } from '#angular/core'
export class YourComponent implements AfterViewInit {
ngAfterViewInit() {
kistenPromise.then(...)
}
}

Categories