[Fixed] Angular ng-content different behaviour while using select

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

Stackblitz

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.

Leave a Reply

(*) Required, Your email will not be published