Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use @ViewChildren/@ContentChildren in combination with router-outlet

I am trying to build this :

┌--------------------------------------------┐
| -Page 1-   -Page 2-   -Page 3-             |
├----------┬---------------------------------┤
| Link 1   | <router-outlet></router-outlet> |
| Link 2   |                                 |
| Link 3   |                                 |
|          |                                 |
|          |                                 |

The list of links on the left depends on the page.

A typical page looks like this :

<div>
  <app-component-1 appAnchor anchorTitle="Link 1"></app-component-1>
  <app-component-2 appAnchor anchorTitle="Link 2"></app-component-2>
  <app-component-3 appAnchor anchorTitle="Link 3"></app-component-3>
</div>

There are some directives appAnchor associated to @Input() anchorTitle: string . I want to automagically capture them and update the left menu.

The problem comes when I try to query those elements through the router-outlet.

So I tried :

@ViewChildren(AnchorDirective)
viewChildren: QueryList<AnchorDirective>;

@ContentChildren(AnchorDirective)
contentChildren: QueryList<AnchorDirective>;

ngAfterContentInit(): void {
  console.log(`AppComponent[l.81] contentChildren`, this.contentChildren);
}

ngAfterViewInit(): void {
  console.log(`AppComponent[l.85] viewChildren`, this.viewChildren);
}

but I always get some empty QueryList :

QueryList {dirty: false, _results: Array(0), changes: EventEmitter, length: 0, last: undefined, …}

I also tried :

@ContentChildren(AnchorDirective, {descendants: true})
@ContentChildren(AnchorDirective, {descendants: false})

Finally, I tried to query the elements at the last moment with :

<router-outlet (activate)="foo()"></router-outlet>

Note : I don't want the children component to send data to the parent component via a service because it causes some ExpressionChangedAfterItHasBeenCheckedError and is considered as a bad practice. I would really prefer to query the links directly from the parent component.

EDIT : Here is a stackBlitz showing the problem. If a @ViewChildren is used inside the component, everything works. But if the @ViewChildren is used from the root component, it fails.

like image 473
Arnaud Denoyelle Avatar asked Jul 09 '19 14:07

Arnaud Denoyelle


2 Answers

There is an on ongoing discussion here about how ContentChildren works, and it is closely related to your problem.

And in this comment it is explained that:

In Angular content projection and queries it means "content as it was written in a template" and not "content as visible in the DOM"

and

The consequence of this mental model is that Angular only looks at the nodes that are children in a template (and not children in the rendered DOM).

Similar to the issue above, components activated via router-outlet are not considered as ViewChildren or ContentChildren. They are just DOM children, and that doesn't mean anything to angular in terms of View and Content queries. There is no way (as of today) that they an be queried with ViewChild or ViewChildren or ContentChild or ContentChildren

My suggestion to solve your issue is to use a combination of activate event and the component property of RouterOutlet to achieve the desired behavior. As such:

Define router-outlet as follows:

<router-outlet #myRouterOutlet="outlet" (activate)='onActivate()'></router-outlet>

And use it as follows:

@ViewChild("myRouterOutlet", { static: true }) routerOutlet: RouterOutlet;
nbViewChildren: number;
links = [];

onActivate(): void {
  setTimeout(() => {
    const ancList: QueryList<AnchorDirective> = (this.routerOutlet.component as any).children;

    this.nbViewChildren = ancList.length;
    this.links = ancList.map(anc => anc.anchorTitle);
  })
}

Here is a working demo with improved typing: https://stackblitz.com/edit/angular-lopyp1

Also note that setTimeout is required because onActivate is fired during routing where the component life cycle hasn't started or finished yet. setTimeout ensures that the component lifecycle has completed, and that the component and underlying queries are ready as well.

like image 58
ysf Avatar answered Oct 15 '22 15:10

ysf


The (activate)="handleActivate($event)" option won't work, as the router-outlet elements won't have been initialized yet. The $event in that case is indeed the component instance, but it's not really helpful here

The ViewChildren, ContentChildren don't seem to work for router-outlet. I didn't think they would, but tested it a good bit with your StackBlitz demo

You'll have to use a service, which is the standard way of doing it, and by far the most flexible. You will be able to get around the ExpressionChangedAfterItHasBeenCheckedError with ChangeDetectorRef.detectChanges(), or better still, use a BehaviorSubject, and next the values from that. In your template subscribe using async pipe, and you won't get those errors

like image 28
Drenai Avatar answered Oct 15 '22 17:10

Drenai