Issue
As we know using ng-content projects content only in last matched ng-content, but the thing I dont understand is that why does it behave the opposite while using select and projects only into the first one.
First Child Component
1
<ng-content select=".my-class"></ng-content> <br>
2
<ng-content select=".my-class"></ng-content>
Second Child Component
1
<ng-content></ng-content> <br>
2
<ng-content></ng-content>
Parent Component
<app-first>
<ng-container class="my-class">CHILD</ng-container>
</app-first>
<br>--------<br>
<app-second>
<ng-container class="my-class">CHILD</ng-container>
</app-second>
Result
1 CHILD
2
--------
1
2 CHILD
Solution
Angular compiler calculates so called ngContentIndex
. You can think of it as a place where transcluded node should be projected.
So, having AppComponent
template
<app-first>
<ng-container class="my-class">CHILD</ng-container>
</app-first>
<br>--------<br>
<app-second>
<ng-container class="my-class">CHILD</ng-container>
</app-second>
Angular gives each node its ngContentIndex
(take a look at parentheses):
<app-first>(null)
<ng-container class="my-class">CHILD</ng-container>(1)
</app-first>
<br>--------<br>(null)
<app-second>(null)
<ng-container class="my-class">CHILD</ng-container>(0)
</app-second>
It means that first CHILD
should go to second ng-content and the second CHILD
to the first.
Why did it happen like this?
The asnwer lies in source code of Angular compiler.
By default <ng-content></ng-content>
is the same as
<ng-content select="*"></ng-content>
For each template Angular knows all its ngContentSelectors
:
FirstComponent ['*', '*']
SecondComponent ['.my-class', '.my-class']
And here is the answer:
const ngContentSelectors = component.directive.template !.ngContentSelectors;
for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i];
if (selector === '*') {
wildcardNgContentIndex = i;
} else {
matcher.addSelectables(CssSelector.parse(ngContentSelectors[i]), i);
}
}
You can see that Angular treats *
specially and updates wildcardNgContentIndex
in a loop so that it will be 1
. For ['.my-class', '.my-class']
array it adds selectors to matches.
Finally, Angular uses this method:
findNgContentIndex(selector: CssSelector): number | null {
const ngContentIndices: number[] = [];
this._ngContentIndexMatcher.match(selector, (selector, ngContentIndex) => {
ngContentIndices.push(ngContentIndex);
});
ngContentIndices.sort();
if (this._wildcardNgContentIndex != null) {
ngContentIndices.push(this._wildcardNgContentIndex);
}
return ngContentIndices.length > 0 ? ngContentIndices[0] : null;
}
which will return saved 1
value for projectable node in FirstComponent
.