8

I'm facing this issue frequently. I have an element as shown

<div class="element-1" *ngIf="isShown"></div>

by default, isShown = false; and by clicking an element, I'm making isShown = true;

Now, in the same click callback function If I try to get element-1 as

$('.element-1'), I am not getting that element because it might not in the DOM immediately when the isShown = true.

I am able to get the same using ngAfterContentChecked. But ngAfterContentChecked called many times.

So, how can I get the element by not using ngAfterContentChecked?

Edit

This is my element

<app-card-kpi-inline-overlay #kpisOverlay class="child-component all-kpis-overlay-wrap {{selectedView}}" [style.left.px]="kpiLeft" *ngIf="data['openKpiDetails']==true" [cardData]="data"></app-card-kpi-inline-overlay>

This is my ts method code

@ViewChild('kpisOverlay') kpisOverlay: ElementRef;

showKpiSection(i, event, card) {
    card['openKpiDetails'] = !card['openKpiDetails'];
    event.stopPropagation();
    if (card['openKpiDetails']) {
        setTimeout(() => { 
            const el: HTMLElement = this.kpisOverlay.nativeElement; 
            console.log(el); // always showing undefined
        }, 0);
    }
}

I am trying to toggle the flag. But the console always printing undefined.

Below is my toggle element

<div (click)="showKpiSection(i, $event, data)">Get element</div>
luiscla27
  • 4,956
  • 37
  • 49
Mr_Perfect
  • 8,254
  • 11
  • 35
  • 62
  • 2
    Using jquery with angular is not usually recommended. Tell us what you are trying to do after selecting the element. Maybe there is a non-jquery way to do it. – charsi Feb 14 '18 at 12:58
  • 1
    Why do you want get an DOM element? Angular is built with data-binding. Get an div by Id or class via JQuery is not recommanded – Pterrat Feb 14 '18 at 12:59
  • I want to apply width dynamically after setting the flag to true. – Mr_Perfect Feb 14 '18 at 13:25
  • I updated my question. Please go through it – Mr_Perfect Feb 14 '18 at 13:33
  • 1
    Does this answer your question? [Event to fire when an angular \*ngIf statement evaluates in template](https://stackoverflow.com/questions/44472771/event-to-fire-when-an-angular-ngif-statement-evaluates-in-template) – luiscla27 Mar 18 '21 at 20:14

4 Answers4

7

I will never say it enough :

Using JQuery with Angular is an anti-pattern. You should not touch the DOM yourself, you should let the framework do it for you.

Now, in a full ANgular way :

<div #firstElement *ngIf="isShown"></div>

In your TS :

@ViewChild('firstElement') firstElement: ElementRef;

If you want the element, in your function, once the value of isShown is set to true, use this

const el: HTMLElement = this.firstElement.nativeElement;

If it doesn't work, trigger a change detection, because it means Angular hasn't finished detecting the changes yet.

EDIT Since your component is in a loop, you should use @ViewChildren instead.

I made a working StackBlitz for you, look at the code, it's basically the same principle, only that the element becomes an array of elements.

  • No, I am unable to get the element. Showing undefined. How can I trigger a change detection? What steps should I do to get the element and add width to it dynamically? – Mr_Perfect Feb 14 '18 at 13:20
  • 1
    to trigger a change detection, the fastest is simply to use a time out with `setTimeout(() => {const el: HTMLElement = this.firstElement.nativeElement;})`. Once you get the element, simply do `el.style.width = el.offsetWidth + 30 + 'px'` –  Feb 14 '18 at 13:25
  • I updated my code. I am still getting undefined value – Mr_Perfect Feb 14 '18 at 13:31
  • Because your condition is `*ngIf="data['openKpiDetails']==true"` but in your code, you never set it to true. This means your element never shows up, that's why you're getting undefined. –  Feb 14 '18 at 13:37
  • `card['openKpiDetails'] = !card['openKpiDetails'];` this statement toggling the variable. Please re check – Mr_Perfect Feb 14 '18 at 13:39
  • `data` is different from `card`. If you use a function or an accessor, it would be good to know. Otherwise, those two are different variables. –  Feb 14 '18 at 13:42
  • Oh sorry. on click I am passing `data` and please consider that both are same – Mr_Perfect Feb 14 '18 at 13:45
  • Ok then, what happens if you put a one second delay into your timeout ? Dos it work then ? –  Feb 14 '18 at 13:46
  • No, `this.kpisOverlay` is printing Component class with its properties. not as DOM element as it is a component itself. `this.kpisOverlay.nativeElement` is printing undefined. – Mr_Perfect Feb 14 '18 at 13:49
  • Because `nativeElement` is the HTML element, `kpisOverlay` is an Angular object (ElementRef). If even a one second delay isn't working, then you have something wrong with your code, that doesn't allow your component to be displayed. is your component on your screen ? can you see it ? –  Feb 14 '18 at 13:51
  • Yes, it is showing but I just want to apply some styles dynamically. For that, I am accessing it. But I am unable to get it as DOM element – Mr_Perfect Feb 14 '18 at 13:53
  • Then I will need to have a **[MCVE](https://stackoverflow.com/help/mcve)**, because you either do something wrong you didn't share on SOF, or you have an unique issue that needs to be reported to the Angular team. –  Feb 14 '18 at 13:55
  • Wait ... is this element in a *ngFor ? @Mr_Perfect –  Feb 14 '18 at 13:56
  • Yes. It is there in a loop. And I tried moving the component into a wrapper as the reference not working on the component selector. Now, it is working but there is little jerk before setting the width in the timeout even I use `0` second delay in timeout – Mr_Perfect Feb 14 '18 at 14:04
  • See, that's why you should post your whole code. **[Here is a stackblitz](https://stackblitz.com/edit/angular-material2-issue-ttpf8y?file=app%2Fapp.component.ts)** to show you the working code. –  Feb 14 '18 at 14:11
  • But there is no *ngIf right and why references not working on component selector? – Mr_Perfect Feb 14 '18 at 14:13
  • Now there is ;) and could you rephrase your question ? –  Feb 14 '18 at 14:15
4

You can take advantage of @ViewChild and @ViewChildren behaviour:

Property decorator that configures a view query. The change detector looks for the first element or the directive matching the selector in the view DOM. If the view DOM changes, and a new child matches the selector, the property is updated.

1. ViewChild approach using a setter

The important part is If the view DOM changes which means that in this case this'll only be triggered when the element is created or destroyed.

First declare a variable name for the element, for the sample i used #element1

<div #element1 class="element-1" *ngIf="isShown"></div>

Then add a @ViewChild reference inside your component:

@ViewChild('element1') set element1(element) {
  if (element) {
     // here you get access only when element is rendered (or destroyed)
  }
}

2. ViewChildren approach using Observables

Another solution is to subscribe to @ViewChildren change observable, instead of using @ViewChild put it like this:

@ViewChildren('element1')
private element1: QueryList<any>;

And then subscribe to it change observable:

element1.changes.subscribe((d: QueryList<any>) => {
  if (d.length) {
    // here you get access only when element is rendered
  }
});

I've preferred the last way because to me it was easier to handle observables than validations inside setter's, also this approach is closer to the "Event" concept.

luiscla27
  • 4,956
  • 37
  • 49
2

You should define your element like this:

<div #element-1 class="element-1" *ngIf="isShown"></div>

And then access it with

@ViewChild('element-1') element1;

in your Component. The example on this page looks close to what you are trying to do: https://angular.io/api/core/ViewChild

Vincent Gagnon
  • 650
  • 2
  • 7
  • 15
0

For me, it works when I remove property static from viewChiled decorator

@ViewChild('mainTextBox', {read: ElementRef, static: true}) public parentTextBoxElement: ElementRef<HTMLElement>;

to

@ViewChild('mainTextBox', {read: ElementRef}) public parentTextBoxElement: ElementRef<HTMLElement>;