44

I've looked at several related posts and documentation, but still can't seem to get expected behavior from @ViewChild.

Ultimately, I'm trying to set the scroll position of a div. This element isn't a component, but a normal div in my HTML.

To accomplish this, I'm trying to use @ViewChild to get the DOM element I need, and set its scroll value. (As an aside, if you know a better way to accomplish this without @ViewChild (or jQuery), answers will be very much appreciated!)

At the moment, @ViewChild only returns undefined. Going through some dummy checks: - I am accessing my element in AfterViewInit - I do not have any other directives like *ngIf or *ngFor on this element.

Here's the controller:

import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';

@Component({
    selector: 'portfolio-page',
    templateUrl: './portfolio-page.component.html',
    styleUrls: ['./portfolio-page.component.scss']
})

export class PortfolioPageComponent implements AfterViewInit{
    @ViewChild('gallery-container') galleryContainer: ElementRef;

    ngAfterViewInit(){
        console.log('My element: ' + this.galleryContainer);
    }
}

And the template:

<div id='gallery-container' class='gallery-image-container'>
    <div class='gallery-padding'></div>
    <img class='gallery-image' src='{{ coverPhotoVm }}' />
    <img class='gallery-image' src='{{ imagepath }}' *ngFor='let imagepath of imagesVm' />
</div>

My output is simple: My element: undefined.

As you can see, I'm currently trying to access the element by ID, but have tried class name as well. Could anyone provide more detail about what the ViewChild selector query is expecting?

I've also seen examples where a hash '#' is used as the selector idendifier that @ViewChild uses -- -- but this causes a template parse error for me with #gallery-container.

I can't think of anything else that could possible be wrong here. All help is appreciated, thanks!

Full code available here: https://github.com/aconfee/KimbyArting/tree/master/client/KimbyArting/components/portfolio-page

Adam
  • 877
  • 2
  • 10
  • 24
  • 1
    Have you tried using property binding? I don't know which scroll property you want, but you can try: `
    `. Then just update `this.someComponentProperty` in your component logic. See also http://stackoverflow.com/a/36224837/215945.
    – Mark Rajcok Sep 01 '16 at 14:51
  • Thanks for adding this! I've actually been looking into this as a way of solving my original problem -- setting the scroll value. I've made a custom directive and am passing it the desired scroll value. I also want the directive to watch for when the parent component changes this value with ngOnChanges. Is ngOnChanges the best way to bind a property between a component and directive? I noticed the example you sent is doing things a bit differently.. could you elaborate or send docs on what they're doing? I didn't realize components have a directives array. What default binding exists there? – Adam Sep 01 '16 at 19:43
  • ngOnChanges is a good way. The example I sent was just to show you an actual binding example (using `[inScrollHeight]` in that case). Don't worry about the content projection and "has changed after it was checked" error in that post. There is no default binding between a component and the directives it specifies in the `directives` array. You have to declare any bindings you want in the HTML template. – Mark Rajcok Sep 01 '16 at 20:03
  • Gotcha, thanks. I gave it a closer second read-over and things made much more sense. – Adam Sep 01 '16 at 21:18
  • 1
    Possible duplicate of [Angular 2 @ViewChild annotation returns undefined](http://stackoverflow.com/questions/34947154/angular-2-viewchild-annotation-returns-undefined) – blo0p3r Dec 14 '16 at 15:56

6 Answers6

70

Try using a ref in your template instead:

<div id='gallery-container' #galleryContainer class='gallery-image-container'>
    <div class='gallery-padding'></div>
    <img class='gallery-image' src='{{ coverPhotoVm }}' />
    <img class='gallery-image' src='{{ imagepath }}' *ngFor='let imagepath of imagesVm' />
</div>

And use the ref name as the argument:

@ViewChild('galleryContainer') galleryContainer: ElementRef;

EDIT

Forgot to mention that any view child thus declared is only available after the view is initialized. The first time this happens is in ngAfterViewInit (import and implement the AfterViewInit interface).

The ref name must not contain dashes or this will not work

Aviad P.
  • 32,036
  • 14
  • 103
  • 124
  • Thanks! I was trying #gallery-container before, which caused a parse error. I didn't realize there was a convention in place. I've seen other peoples' working examples which don't have this sort of ref identifier. Is this the only thing @ViewChild expects? – Adam Aug 31 '16 at 23:30
  • Got your edit, thanks! Although, in my code above, you'll see I've already implemented that. I've also tried using the ref syntax before... just didn't realize that it *must* be camel case. – Adam Aug 31 '16 at 23:31
  • It _must_ be camel case? So pascal case (`#GalleryContainer`) is invalid? – Dustin Cleveland Mar 21 '17 at 15:50
  • 1
    I think only the `-` is causing problems, not other casings – Amit Sep 12 '17 at 08:03
  • FWIW, the interface is (now) named `AfterViewInit`, without the `On` prefix. – Cedric Reichenbach Nov 06 '17 at 08:17
  • The second part of this answer was the issue for me. Moving code to the ngAfterViewInit fixed my issue. – Donovan Sep 07 '20 at 14:08
  • For everyone, who use multiple viewchilds: If you still get an error, although everything should be already initialized, then you can add a recursive condition like: openViewChild(id:number):void { if(this.yourViewChild){ yourFunction(); }else{ setTimeout(() => { openViewChild(id); }, 300); } – AndyNope Jun 18 '21 at 14:39
  • omg how did I miss this syntax, always assumed that only the id was necessary. thanks – Marc Sloth Eastman Oct 25 '21 at 15:35
29

Sometimes, if the component isn’t yet initialized when you access it, you get an error that says that the child component is undefined.

However, even if you access to the child component in the AfterViewInit, sometimes the @ViewChild was still returning null. The problem can be caused by the *ngIf or other directive.

The solution is to use the @ViewChildren instead of @ViewChild and subscribe the changes subscription that is executed when the component is ready.

For example, if in the parent component ParentComponent you want to access the child component MyComponent.

import { Component, ViewChildren, AfterViewInit, QueryList } from '@angular/core';
import { MyComponent } from './mycomponent.component';

export class ParentComponent implements AfterViewInit
{
  //other code emitted for clarity

  @ViewChildren(MyComponent) childrenComponent: QueryList<MyComponent>;

  public ngAfterViewInit(): void
  {
    this.childrenComponent.changes.subscribe((comps: QueryList<MyComponent>) =>
    {
      // Now you can access the child component
    });
  }
}
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
Marco Barbero
  • 1,460
  • 1
  • 12
  • 22
  • 8
    This is a very important point about *ngIf. If you are loading data asynchronously and hiding part of your UI then not only will it not be visible but it cannot be bound since it's not there! – Simon_Weaver Jan 30 '18 at 04:15
  • Thank you @Simon_Weaver. The use of *ngIf in this case has created me some problems that I solved definitively with the application of this solution. – Marco Barbero Jan 31 '18 at 10:46
2

Subscribing to changes on

@ViewChildren(MyComponent) childrenComponent: QueryList<MyComponent>

confirmed working, combined with setTimeout() and notifyOnChanges() and careful null checking.

Any other approach produces unreliable results and is hard to test.

Paul Karam
  • 4,052
  • 8
  • 30
  • 53
1

I had a similar issue. Unlike you, my @ViewChild returned a valid ElementRef, but when I tried to access its nativeElement, it was undefined.

I resolved it by setting the view id like #content, not like #content = "ngModel".

<textarea type = "text"
    id = "content"
    name = "content"
    #content
    [(ngModel)] = "article.content"
    required></textarea>
Yamashiro Rion
  • 1,778
  • 1
  • 20
  • 31
0

This is your .ts file code for using DOM elements by ViewChild

import { Component, AfterViewInit, ViewChild, ElementRef } from '@angular/core';

@Component({

    selector: 'portfolio-page',
    templateUrl: './portfolio-page.component.html',
    styleUrls: ['./portfolio-page.component.scss']
})

export class PortfolioPageComponent implements AfterViewInit{

    @ViewChild('galleryContainer') galleryContainer: ElementRef;

    ngAfterViewInit(){
        console.log('My element: ' + this.galleryContainer);
    }
}

This is your .html file code

    <div #galleryContainer class='gallery-image-container'>
    <div class='gallery-padding'></div>
    <img class='gallery-image' src='{{ coverPhotoVm }}' />
    <img class='gallery-image' src='{{ imagepath }}' *ngFor='let imagepath of imagesVm' />
</div>

Hope it helps!

David García Bodego
  • 1,058
  • 3
  • 13
  • 21
Vikki
  • 31
  • 1
  • 1
  • 8
-1

Another possible scenario is when you are using typescript in angular or ionic framework and calling the ViewChild element from a function having javascript like signature. Please use a typescript like function signature instead (e.g. using fat arrow =>). For example

accessViewChild(){
    console.log(this.galleryContainer.nativeElement.innerHTML);
}

would show print undefined however

accessViewChild = ()=>{
    console.log(this.galleryContainer.nativeElement.innerHTML);
}

would print the inner html.

Pal
  • 989
  • 10
  • 23