Aparigraha
Aparigraha

Reputation: 177

@Viewchild is not working inside Directive, but working in Component

I am just playing around with @ViewChild/@ContentChild, and I was surprised to see that the @ViewChild is not working inside directive and is working fine for component.But in Directive its not working. I tried with AfterViewInit hook, so life cycle hook is not the reason. Something else is the issue here,Please find the code below.

app.component.html

<div appMain >
  <div #testContentDiv style="background-color: grey">
    <p>This is the first p tag</p>
    <p>This is the second p tag</p> 
  </div>
  <div #testViewDiv style="background-color: yellow">
    <p>This is the first p tag</p>
    <p>This is the second p tag</p>
  </div>
  <app-test-child></app-test-child>
</div>

test-dir.ts --Directive

import { Directive, ViewChild, ElementRef, OnInit, AfterViewInit, AfterContentInit, ContentChild } from '@angular/core';

@Directive({
  selector: '[appMain]'
})
export class MainDirective implements OnInit, AfterContentInit, AfterViewInit {

  constructor() { }
  // tslint:disable-next-line:member-ordering
  @ContentChild('testContentDiv') testContent: ElementRef;
  @ViewChild('testViewDiv') testView: ElementRef;
  ngOnInit() {
    //Called after the constructor, initializing input properties, and the first call to ngOnChanges.
    //Add 'implements OnInit' to the class.
    // console.log(this.test.nativeElement);

  }

  ngAfterContentInit() {
    //Called after ngOnInit when the component's or directive's content has been initialized.
    //Add 'implements AfterContentInit' to the class.
    console.log('Content Div: ngAfterContentInit:  ' + this.testContent.nativeElement);
    // console.log('View Div: ngAfterContentInit: ' + this.testView.nativeElement);


  }

  ngAfterViewInit() {
    //Called after ngAfterContentInit when the component's view has been initialized. Applies to components only.
    //Add 'implements AfterViewInit' to the class.
    console.log('Content Div:ngAfterViewInit: ' + this.testContent.nativeElement);
    console.log('View Div: ngAfterViewInit: ' + this.testView.nativeElement);

  }
}

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = "App works";

  constructor() {
  }
}

Upvotes: 10

Views: 8687

Answers (3)

Shaddy Zeineddine
Shaddy Zeineddine

Reputation: 516

Angular 18

Using @ViewChildren in an attribute directive does not work because attribute directives don't have templates, so there are no children to query. You need to use @ContentChildren instead. Make sure to set descendants equal to true if the component you want to select isn't a direct descendant of the element with the attribute directive.

import { AfterViewInit, Directive, QueryList, ContentChildren } from '@angular/core';
import { TargetComponent } from './target.component';

@Directive({
  selector: '[appSubject]',
  standalone: true,
})
export class SubjectDirective implements AfterViewInit {
  @ContentChildren(TargetComponent, { descendants: true }) targets!: QueryList<TargetComponent>;

  ngAfterViewInit(): void {
    console.log('targets = ');
    console.log(this.targets);
  }
}

Additional context: https://github.com/angular/angular/issues/58642

Upvotes: 2

dudewad
dudewad

Reputation: 13943

At least as of Angular v6.x:

Accoring to the Angular source code for directives, it is indeed possible to select children. However, the standard way of just using the @ViewChildren or @ContentChildren decorators does not appear to work for me. Also, I am unable to get @ViewChildren to work despite the docs.

@ContentChildren, however, does work for me. You need to decorate the Directive itself with the queries property as such (this directive is dumbed down for clarity, you'll still need a selector and other stuff to make it work):

@Directive({
  queries: {
    // Give this the same name as your local class property on the directive. "myChildren" in this case
    myChildren: new ContentChildren(YourChild),
  },
})
export class MyDirective implements AfterContentInit {
  // Define the query list as a property here, uninitialized.
  private myChildren: QueryList<YourChild>;

  /**
   * ngAfterContentInit Interface Method
   */
  public ngAfterContentInit() {
    // myChildren is now initialized and ready for use.
  }
}

This suffices for me so I'm not going to waste more time figuring out why ViewChildren doesn't work. My understanding of the difference between ViewChildren and ContentChildrenis that ContentChildren selects from <ng-content> tags, where ViewChildren selects straight from the view itself. So, the behavior seems backwards to me, but there is probably a justification for it.

As expected, ContentChildren are not available until the ngAfterContentInit hook, so don't let that one bite you.

Upvotes: 11

Madhu Ranjan
Madhu Ranjan

Reputation: 17944

There are three kinds of directives in Angular:

Components — directives with a template.

Structural directives — change the DOM layout by adding and removing DOM elements.

Attribute directives — change the appearance or behavior of an element, component, or another directive.

So by definition Components are the only directive with a template so you can find @ViewChild only for components.

Read more about it here.

Hope this helps!!

Upvotes: 4

Related Questions