Reputation: 1173
I'm trying to run some DOM manipulation code when an observable resolves and an ngIf conditional is therefore resolved in the view, but every time I'm trying to access it, it returns undefined. Here is a snapshot of my code:
View:
<div *ngIf="data?.data_child" id="data-element">
... data here
</div>
Controller:
public data;
constructor(private _async: AsyncService){}
ngOnInit() {
this._async.subscribe( result => {
if( result ) {
// data contains data_child
this.data = result;
// run DOM manipulation on the element
if( this.data.data_child ) {
// error is thrown here as the element is undefined
$('#data-element').scrollTop($('#data-element')[0].scrollHeight);
}
}
}
}
I would like to mention that I have tried creating another subscription inside ngAfterViewInit and calling it there but still, the same error occurs.
The only thing that kind of worked was emitting an event when the data is loaded which I then subscribe to and trigger the DOM manipulation methods. However, it only works the first time ngOnInit is triggered as further routing to that component no longer trigger the event( there is no reuseComponent functionality ).
I also tried to use @ViewChild and set a template variable which is then used in the controller, but it's still throwing the same error.
Any ideas? Thanks!
Upvotes: 0
Views: 603
Reputation: 23
First you absolutely should use ViewChild, rather than JQuery.
<div #data-element>
... data here
</div>
in the template and
@ViewChild('data-element') dataElement;
in the code behind. Angular is an all encompassing framework. You really kind of have to embrace it and it's way of doing things. There is, of course, a learning curve, but it's well worth it, and there's not a lot, JQuery can add to it.
Don't forget to add 'ViewChild' to your imports directive. e.g.
import { Component, Input, Output, OnInit, OnChanges, ViewChild, ElementRef, Renderer2, HostListener, EventEmitter, AfterViewInit} from '@angular/core';
However, because the element is toggled with ngIf, you'll still get the same error, because the element doesn't exist when the component is initialised, so the view child is created with an undefined ElementRef.
There are a few solutions.
If you running Angular 8, you can simply tell it that the element is not static and when it exists the view child will receive the ElementRef:
@ViewChild('data-element', { static: false }) dataElement: ElementRef;
Prior to Angular 8, the best way is to place the conditional element in a child component, and pass any required data in as a input parameters:
<app-data-element *ngIf="data?.data_child"> </app-data-element>
with the the child component rendering your data. You can pass your data into it via @input parameters:
@input() data: DataStructure;
@input() dob: Date;
You can also pass data back via @Output events.
@Output() dataChanged: EventEmitter<DataStructure> = new EventEmitter<DataStructure>();
this.dataChanged.emit(data);
The element always exists in the child component so the ElementRef will always be valid.
And finally, you could also just toggle the style display property instead. The component always exists in the DOM and the view child will always have a valid ElementRef. Obviously it always uses resources and contributes to load overhead, where you ever display it or not:
<div [style.display] = "displayData()" id="data-element">
... data here
</div>
With your code behind being something like:
@ViewChild('data-element') dataElement;
public displayData() {
if (data?.data_child) {
return 'block';
}
return 'none';
}
(This will work with JQuery selectors as well, but you really shouldn't.)
Upvotes: 0
Reputation: 6535
Use the ngAfterViewInit
lifecycle hook to manipulate DOM elements. Are you using jQuery to just get the height of the element? You can do that using native Angular code: this.elementRef.nativeElement.offsetHeight
.
jQuery should be used with caution, but if you have to use it you could make a directive which use #data-element
as its selector:
import { Directive, ElementRef, AfterViewInit } from '@angular/core';
declare var $: any
@Directive({
selector: '#data-element'
})
export class DataElementDirective implements AfterViewInit {
constructor(private el: ElementRef) {
}
ngAfterViewInit() {
$(this.el.nativeElement).scrollTop($('#data-element')[0].scrollHeight);
}
}
But again there should be no need to resort to jQuery for this kind of thing.
Upvotes: 1
Reputation: 8165
You have to give the change detection a chance to detect something first and the rendering to happen.
The moment you set this.data = result;
your change detection will kick in and will order a rerendering of the affected view ( /-child ). This will take way longer then it takes to evaluate your next if
case which means at the moment you call your jQuery
function the DOM Element
isn't accessible yet.
A quick and dirty solution would be to use setTimeout
:
this.data = result;
if( this.data.data_child ) {
setTimeout(() => {
$('#data-element').scrollTop($('#data-element')[0].scrollHeight);
});
}
You can use this in your example, but i should mention that this isn't considered a good practice.
A better approach would probably be to use maybe an Observable
which will notify you when your data has been assigned or through another kind of logic, for example set a fixed anchor you want to scroll to, which doesn't rely on dynamically added elements and scroll to that instead.
On the other hand, to imitate events like scrollTo, focus, blur on dynamically generated elements are often tricky if you want to trigger them programmatically and want to do it the "angular" way. Speaking of which, a directive which serves as a container for your data could be another good way to implement things like this without using timeouts.
Upvotes: 1