How to get access to nativeElements on ngOnInit? - javascript

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 }

Related

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

Passing value to child component after it's added to the list on parent component - Angular

I'm working on small Angular project, and I want to pass value from parent to child after it is addedd to the list, so it goes like this:
new-component emits item to the list-component
and after it's addedd to the list, lets move it forward to view-component, so I can see recently addedd item in case I want to edit something--
And everything works fine until I want to see it on view-component (it's successfully added to the list etc.) here is my code:
on a new component item is emitted to list:
onSubmit() {
this._viewService.save(this.view)
.subscribe(
(data: View) => {
this.onSave.emit(data);
}
}
As list is parent list is listening for this emitter.
<view-new #addNew (onSave)="onAdd($event)"></view-new>
After it is emitted, its available in onAdd method, and until this part everything is ok:
onAdd(view: View) {
this.receivedView = view;
// And here I'm opening viewDisplay compnent which displaying info about recently addedd data
this.viewDisplay.show();
}
Also on my list which is parent component is also included display component, which needs to display info about recently addedd item:
<view-display #displayView [view]="receivedView"></view-display>
And when I open this component by calling this.viewDisplay.show();
#Input() view: View;
Which is part of display component is allwayas empty :
show() {
$('#' + this.id).modal('show');
console.log("added - selected:view", this.view);
}
this.view which should be filled onAdd method from parent and passed here is allways empty.. I dont know how..
Thanks guys
Cheers
you are updating field recievedView in a parent, which is bound to view field in a child. This binding is done by angular change detection mechanism. In your onAdd method you are trying to call viewDisplay.show() method synchroniously, before the change detection is done, and, because of this field is not updated yet. The simplest solution would be to assign the field of a child in place like
onAdd(view: View) {
this.receivedView = this.viewDisplay = view;
this.viewDisplay.show();
}
but there are surely better options which depend on your project
Angular needs to know that the value of view has changed. You usually do this by defining a setter. By defining a setter, changes to an input variable can be tracked. You also need to define a private property to save the value in.
private _view: any;
#Input()
get view(): any) {
return this._view;
}
set view(view: any) {
this._view= view;
// Do other stuff when "view" was updated
}
You can also handle the communiation via the service pattern.

Angular - Cannot see how Angular2 auto updates data from service

I am an Angular2 beginner, creating a test note-taking app. The app is simple: I have a NotesContianerComponent which connects to a NoteService which has the getNotes() and addNote() method.
In the NotesContainerComponent, I am storing the data returned from service in a member myNotes and on ngOnInit event, refreshing the array. I am not touching that array anywhere in that code as of now.
Now, when I add a new note using the form and call the service's addNote() method from the NotesContainerComponent, the note does get added to the backend mock notes array, but at the same time the UI (i.e. NotesContainerComponent) gets updated with the new note automatically! I am not doing anything to refresh it manually.
To investigate, I added a console.log() to the getNotes method of the service, however it is being only called the first time, possibly by ngOnInit hook.
What I cannot figure out is, how does Angular2 know about the new note without even querying the service automatically? And how to stop this? Any clues will be appreciated.
NotesContainerComponent code for reference:
export class NotesContainerComponent implements OnInit {
constructor(
private noteService: NoteService
) { }
addNote(newNote):void {
this.noteService.add(newNote);
}
myNotes: Notes[];
ngOnInit() {
this.noteService.getNotes()
.then(notes => this.myNotes = notes);
}
}
If you are storing your mock data in an Object or an Array and this.myNotes's reference is that Objects reference. When your mock datas content changes, all the references will change too. This is because objects are mutable in javascript.

Angular2 Child component destroyed unexpectedly in ngFor

So i have this Component of a from with an #Output event that trigger on submit, as follows:
#Component({
selector: 'some-component',
templateUrl: './SomeComponent.html'
})
export class SomeComponent{
#Input() data: any;
#Output() onSubmit: EventEmitter<void> = new EventEmitter<void>();
constructor(private someService: SomeService) {}
submitForm(): void{
this.someService.updateBackend(this.data, ()=>{
this.onSubmit.emit();
});
}
}
I'm using an ngFor to create multiple elements of this Component :
<template let-data ngFor [ngForOf]="dataCollection">
<some-component [data]="data" (onSubmit)="doSomthing()"></some-component>
</template>
The last missing part is the service used on submitting:
#Injectable()
export class SomeService{
constructor() {}
updateBackend(data: any, callback: () => void): void{
/*
* updating the backend
*/.then((result) => {
const { errors, data } = result;
if (data) {
callback();
}
})
}
}
At the beginning of the submitForm() function, the this.onSubmit.observers is an Array containing one observer, like it should be.
As soon as it reaches the callback method, where the this.onSubmit.emit() is invoked, the this.onSubmit.observers is an Array containing ZERO observers.
I'm experiencing two very weird behaviors:
If i remove the actual calling to update the backend in SomeService.updateBackend it works perfectly fine, and the observers still is an Array containing one observer!
If i keep the actual calling to the backend BUT not using ngFor and displaying only one <some-element> it also works perfectly fine, keeping one observer in the this.onSubmit.observers within the callback scope!
Any idea what am i doing wrong?
Thanks in advance!
Update:
Thanks to #StevenLuke's comment about logging the ngOnDestroy of SomeComponent I found out that it is being destroyed before the emit.
Actually, the first thing it is doing when the SomeService.updateBackend finishes is Destroying all the instances of this component and recreate them!
This is what makes the observers change! Why would that happen?
If you provide a trackBy function in your *ngFor to identify items in your dataCollection, it will not destroy and init. Your template would be:
<some-component *ngFor="let data of dataCollection;trackBy:trackByFunction"
[data]="data" (onSubmit)="doSomthing()"></some-component>
And the trackByFunction would look like:
trackByFunction(index, item) {
return item ? item.id : undefined;
}
So even though an item in your dataCollection is a fresh object, if its id matches an id in the previous collection, *ngFor will update [data] but not destroy and init the component.
Thanks to #GünterZöchbauer comments I found out the case was that the data the ngFor is bound to was being replaced by a new instance as I updated the backend, hence, it rerendered it's child Components causing reinitializing (destory + init) of them, which made the instance of the Component to be overwritten.
In order to solve this issue i had to place the dataCollection in a separate service, getting it for the parent component ngOnInit, saving it from causing a rerender of the ngFor, and fetch its data again only after the execution of the Child Components ended
Hope it'll be helpful to somebody!

Angular2 change detection not working after callback

I am quite new to angular2 and I have a problem with the change detection.
At the loading of my page, I need to call some API in order to get the information to construct my web page. What I do is that when I receive this information (which is contained in an array) I want to iterate through it using *ngFor. This is my code for a course component.
import {Component,Input} from 'angular2/core';
import {courseCompDiagram, sepExInWeeks} from "../js/coursesTreatment.js";
import {getSampleWeeks} from "../js/courseMng.js";
#Component({
selector: 'course',
directives:[Exercises],
template: `
<div class="course">
<h2>{{aCourse.name}}</h2>
<div class='diag-container row'>
<div id="Completion{{aCourse.name}}"></div>
<div *ngFor="#week of weeks"> {{week.weekNb}} </div>
</div>
</div>`
})
export class Course{
//This is inputed from a parent component
#Input() aCourse;
this.weeks = [];
ngAfterViewInit(){
//I call this method and when the callbacks are finished,
//It does the following lines
courseCompDiagram(this.aCourse, function(concernedCourse){
//When my API call is finished, I treat the course, and store the results in weeks
this.weeks = sepExInWeeks(concernedCourse.course.exercises);
});
//This is not supposed to stay in my code,
//but is here to show that if I call it here,
//the weeks will effectively change
this.weeks = getSampleWeeks();
}
}
So first of all, I would like to know if it's normal that angular2 doesnt detect the fact that this.weeks changed.
Then I don't know if I should us the ngAfterViewInit function to do my work in. The problem is that I began doing that because in my courseCompDiagram I need to use jquery to find the div containing the id Completion[...] and modify it (using highcharts on it). But maybe I should do all this at some other point of the loading of the page ?
I tried using ngZone and ChangeDetectionStrategy as stated in this topic but I didn't manage to make it work on my case.
Any help is appreciated, even if it doesn't completely solves the problem.
export class Course{
//This is inputed from a parent component
#Input() aCourse;
this.weeks = [];
constructor(private _zone:NgZone) {}
ngAfterViewInit(){
//I call this method and when the callbacks are finished,
//It does the following lines
courseCompDiagram(this.aCourse, (concernedCourse) => {
//When my API call is finished, I treat the course, and store the results in weeks
this._zone.run(() => {
this.weeks = sepExInWeeks(concernedCourse.course.exercises);
});
});
//This is not supposed to stay in my code,
//but is here to show that if I call it here,
//the weeks will effectively change
this.weeks = getSampleWeeks();
}
}
You should use arrow functions to be able to use lexical this, as described below:
courseCompDiagram(this.aCourse, (concernedCourse) => {
// When my API call is finished, I treat the course,
// and store the results in weeks
this.weeks = sepExInWeeks(concernedCourse.course.exercises);
});
As a matter with raw callbacks, the this keyword doesn't correspond to your component instance.
See this link for more hints about the lexical this of arrow functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions.
Otherwise I have a sample comment regarding your code. You should leverage observables for your HTTP calls. It doesn't seem the case in your code as far as I can see...

Categories

Resources