Promise for *ngFor in Angular2 - javascript

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

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.

Execute function after DOM has finished rendering

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();
}
}

Get unique results from observable

I have a component that fetches data from a service and pushes data to an array upon button click. If that data already exists in the array, I don't want it to be pushed again.
import { ImportResults } from '../shared/mass.interface';
import { Component, Input, OnChanges, OnInit } from '#angular/core';
import { MassEmpService } from '../shared/mass.service';
#Component({
selector: 'app-employee-selection',
templateUrl: './employee-selection.component.html',
styleUrls: ['./employee-selection.component.css']
})
export class EmployeeSelectionComponent implements OnInit {
// Define our search results
public searchResults: ImportResults[] = [];
constructor(
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.subscribe(
data => (this.searchResults.indexOf(data) === -1 ? this.searchResults.push(...data) : '')
);
}
}
Is there any quick way to test if an identical object already exists in an array without having to look for a specific key or value?
Scenario
I enter a username into a search field and press "search". It provides me the results, I push them to an array and then use *ngFor to loop over them and display them. However, if I then searched by supervisor, I don't want anyone thats already in the results to appear again, only new data that has not yet been seen.
I know this is more of a generic Javascript question but I am curious if ES6 or angular has anything short that will accomplish this?
Try to use underscore.js and findIndex method.
The code would be something like that:
import * as _ from 'underscore';
/*
rest of your code
*/
export class EmployeeSelectionComponent implements OnInit {
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.subscribe(data => {
if (this.searchResults.findIndex(elem => _.isEqual(elem,data)) == -1)
this.searchResults.push(data);
});
}
/*
rest of your code
*/
}
Other solution would be to use .filter() method, but here you need to traverse all entries as well:
ngOnInit() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults.filter(data => {
return this.searchResults.findIndex(elem => _.isEqual(elem,data)) == -1})
.subscribe(data => this.searchResults.push(data));
}
I think it could be also good, to immediatelly reject objects with the same reference:
this._massEmpService.importedResults.filter(data => data != data).... //another chain here
You could also try to experiment with _.indexOf: http://underscorejs.org/#indexOf
try below myaaray is your array wityh duplicate entries and uniue array will contain array without duplicate entries
var unique = myArray.filter((v, i, a) => a.indexOf(v) === i);

How to update observable array on load more

I am trying to update Observable array after click on load more but it result in multiple call to API.
Here is my html:
<app-productlistother *ngFor="let item of favs$ | async" [item]="item"></app-productlistother>
<a (click)="loadMore()">Load More</a>
Component:
ngOnInit() {
this.favs$ = this._favoriteService.getFavorites(this.pageNum);
}
private loadMore() {
this.pageNum = this.pageNum + 1;
this.favs$ = this.favs$.merge(this._favoriteService.getFavorites(this.pageNum));
}
And service:
getFavorites(page): Observable<Response> {
return this._http.get(environment.baseUrl + 'favorite?page=' + page)
.map((result: Response) => result.json().data);
}
How can I solve this? On each new request to getFavorites(page) I need to push new results on bottom of array...
#Injectable()
export class MyBooleanService {
myBool$: Observable<any>;
private boolSubject: Subject<any>;
constructor() {
this.boolSubject = new Subject<any>();
this.myBool$ = this.boolSubject.asObservable();
}
...some code that emits new values using this.boolSubject...
}
Then in your component you would have something like this:
#Component({...})
export class MyComponent {
currentBool: any;
constructor(service: MyBooleanService) {
service.myBool$.subscribe((newBool: any) => { this.currentBool = newBool; });
}
}
Now depending on what you need to do with that value you may need to do chnages in your component to update, but this is the gist of using an observable
Another option is you use the async pipe within your template instead of explicitly subscribing to the stream in the constructor. Again though, that depends on what exactly you need to do with the bool values.
So you have no HTML and <app-productionother> template is just {{item.name}} ? In that case, why not structure it like this:
<ng-template *ngFor="let item of favs$ | async">
<app-productlistother [item]="item"></app-productlistother>
</ng-template>
This keeps the async pipe off the repeating element so the async pipe isn't being repeated on each iteration of item. I think that's why it's being called multiple times (likely the same number as you have items).
Also, if merge isn't working out, try concat. Concat will wait until the first observable is finished emitting and then will join the second observable. Merge doesn't wait for the first observable to finish. I imagine you'd want your data to load in order if you click load more and not have initial data interweaved with the second batch, or so on.

Categories