Viewchild / Viewchildren in a Directive

Asked

Viewed 456 times

1

I’m using Angular 7.

My goal is to create a Directive to control the focus of some elements with the arrow keys (in this example, a side menu).

Let’s assume I have the following html:

<ul list>
  <li>
    <a #item routerLink="home">Home</a>
  </li>
  <li>
    <a #item routerLink="about">Sobre</a>
  </li>
</ul>

I created a Directive in Angular with the name Listdirective, and it is already declared in Appmodule.
In this Directive, I would like to receive all the elements that have the attribute #item. I imagine I can do that with the @ViewChildren('item'), but he’s returning me undefined.

Follows code:

import { AfterViewInit, Directive, QueryList, ViewChildren } from "@angular/core";

@Directive({
    selector: '[list]'
})

export class ListDirective implements AfterViewInit {
    @ViewChildren('item') items: QueryList<any>;

    ngAfterViewInit() {
        console.log(this.items); // undefined
    }
}

When I try to do this using a component instead of a Directive, it works perfectly.
This example I gave is a little different than what I’m trying to do (although I’ve tried it that way). I think the ideal is for me to have two Directives: the List (which would be in the ul tag) and the Listitem (which would be in the links), because that way I could better manipulate the events of the links.

Using the @ViewChildren(ListItem) items: QueryList<ListItem> also did not work, returning me Undefined. It seems that the problem is really the fact that I’m trying this on a Directive, but I haven’t found anywhere the information that it’s impossible to do this with Directives.

EDITION

To continue my project, I solved the problem by using a component instead of a Directive, since the latter was not accepting Viewchildren.
I created a class that does the work, and whenever a component has a list of the type, just inherit from that class.

I would still like to do only with Directives, only moving to html, so the question remains:

Viewchildren can be used in a Directive?

1 answer

1


I managed to do what I wanted using only Directives.

I created two Directives: A List and the Listitem.

At first, I used the exportAs as an option in the decorator, so I could create a reference to the object in the template.

In the second, I created an input that receives a List, so that I can make both Directives communicate.

The code of the Listdirective:

@Directive({
  selector: 'list',
  exportAs: 'list'
})
export class ListDirective {
  items: ListItemDirective[] = [];

  addItem(item: ListItemDirective) {
    this.items.push(item);
  }

  removeItem(item: ListItemDirective) {
    let idx = this.items.indexOf(item);

    this.items.splice(idx, 1);
  }
}

The code of the Listitemdirective:

@Directive({
  selector: 'list-item'
})
export class ListItemDirective implements AfterViewInit {
  @Input('list-item') list: ListDirective;

  ngAfterViewInit() {
    this.list.addItem(this);
  }

  ngOnDestroy() {
    this.list.removeItem(this);
  }
}

And finally, the template:

<ul list #myList="list">
  <li>
    <a routerLink="home" [list-item]="myList">Home</a>
  </li>
  <li>
    <a routerLink="about" [list-item]="myList">About</a>
  </li>
</ul>

And that’s it. In the Listdirective object I have the reference of all the items, and in all the items I have the reference of the list.

EDITION: 10/02/2019

I found a simpler way to do that. In the Listitemdirective, you can inject the Listdirective through the decorator @Host(). Thus, it is not necessary to receive the Listdirective through an input, which greatly simplifies the use of these Directives in a template.

Listdirective

@Directive({
  selector: 'list'
})
export class ListDirective {
  items: ListItemDirective[] = [];

  addItem(item: ListItemDirective) {
    this.items.push(item);
  }

  removeItem(item: ListItemDirective) {
    let idx = this.items.indexOf(item);

    this.items.splice(idx, 1);
  }
}

Listitemdirective

@Directive({
  selector: 'list-item'
})
export class ListItemDirective implements OnInit, OnDestroy {
  constructor(@Host() private list: ListDirective) {}

  ngOnInit() {
    this.list.addItem(this);
  }

  ngOnDestroy() {
    this.list.removeItem(this);
  }
}

Template

<ul list>
  <li>
    <a routerLink="home" list-item>Home</a>
  </li>
  <li>
    <a routerLink="about" list-item>About</a>
  </li>
</ul>

EDITION (15/07/2019)

I just discovered a simpler solution (and probably the right one).

At Directive parent, just add a property with the decorator ContentChildren of the kind QueryList<directiveFilho>.

This variable will be available for use after the content is initialized. To access it as soon as possible, we should use callback ngAfterContentInit.

To react to changes, the QueryList has a property called changes, who is an observable.

This variable will be automatically updated and its elements will be in the order they appear in the template.

example:

Listdirective

@Directive({
  selector: 'list'
})
export class ListDirective implements AfterContentInit {
  @ContentChildren(ListItemDirective) items: QueryList<ListItemDirective>;

  ngAfterContentInit() {
    // this.items estará disponível aqui
  }
}

Listitemdirective

@Directive({
  selector: 'list-item'
})
export class ListItemDirective {

}

Template

<ul list>
  <li>
    <a routerLink="home" list-item>Home</a>
  </li>
  <li>
    <a routerLink="about" list-item>About</a>
  </li>
</ul>
  • Thank you for sharing your solution

Browser other questions tagged

You are not signed in. Login or sign up in order to post.