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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With