Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Component Dynamic Compile - ng-content with select not working

I have some components that are compiled dynamically using the resolveComponentFactory.

I do this like so....

const embeddedComponentElements = this.hostElement.querySelectorAll( selector );

const element = embeddedComponentElements[ 0 ];

// convert NodeList into an array, since Angular doesn't like having a NodeList passed
const projectableNodes = [
  Array.prototype.slice.call(element.childNodes),
];

const factory = componentFactoryResolver.resolveComponentFactory( Component );

const embeddedComponent = factory.create(
 this.injector,
 projectableNodes,
 element,
);

But I have a problem that i'm struggling to solve....

This works as expected

<div>
 <ng-content></ng-content>
</div>

This does NOT work as expected:

<div>
 <!-- I get all the content here -->
 <ng-content select="h1"></ng-content>
</div>
<div>
 <!-- I don't get any content here -->
 <ng-content></ng-content>
</div>

It seems that the ng-content select attribute does not work with dynamically compiled content.

Any ideas what I'm doing wrong or is this a bug?

I'm using Angular 8 if that helps.

Update: I've reproduced this issue in stackBlitz here: https://stackblitz.com/edit/angular-ivy-givxx6

like image 215
Ewan Avatar asked Jan 18 '26 20:01

Ewan


1 Answers

I haven't really figured out the projectable nodes parameter yet in combination with ng-select.

But if you check my stackblitz you can see it working.. Sort of.

What I found out that happens:

  1. The ng-select are processed upside down (I think). Which means it first processes the last select in the template and checks the last array in the 2d projected nodes array
  2. When you specify a select query, and it cannot find it in the corresponding array, it just shows all items in the array
  3. The first ng-select corresponds to the first array in the 2d array.
  4. If you have a node that is in a select query, and also add this to a next array, it won't be found in the first select, and be added to the next one for some reason. The other way around is no issue.

So if you have this as dynamic html:

<hello>
  <span class="title">
    Title
  </span>
  <span class="another-title">
    Another title
  </span>
  The rest here of the content......
  <div>Something else here</div>
</hello>

You should make your nodes the following

const projectableNodes = [
  [ nodes[1] ], // span class="title"
  [ nodes[3] ], // span class="another-title"
  [ nodes[0], nodes[2], nodes[4], nodes[5]]
];

To correspond to a template like this:

<h1 class="hello-title">
  <ng-content select=".title"></ng-content>
</h1>
<h2 class="hello-title">
  <ng-content select=".another-title"></ng-content>
</h2>
<div class="hello-content">
  <ng-content></ng-content>
</div>

To explain point 4 (and perhaps point 1), if you do:

const projectableNodes = [
  [ nodes[1], nodes[3] ],
  [ nodes[3] ],
  [ nodes[0], nodes[2], nodes[4], nodes[5]]
];

There are no issues. If you do it the other way around:

const projectableNodes = [
  [ nodes[1] ],
  [ nodes[3], nodes[1] ],
  [ nodes[0], nodes[2], nodes[4], nodes[5]]
];

It will never project nodes[1] at the correct position, but it will somehow be appended to the 2nd <ng-content> with the 'another-title'. I have found this answer, which has some upvotes, but it doesn't make it very clear to me how the mechanism works, or in what way you will be able to make this dynamic.

I hope this gives you some insight though, and perhaps you can come up with a dynamic solution and post your own answer. Or perhaps this was just all the info you need, and I actually helped you out :D who knows


Looking a bit more into the source code, you can find out you the amount of ng-content you have in your element. This is defined by the factory.ngContentSelectors. In my stackblitz this is:

0: ".title"
1: ".another-title"
2: "*"

You can then use the Element.matches() method to select that content, and pass the rest to the wildcard. You can then make it dynamic using the following logic:

const nodes = Array.from(element.childNodes);
const selectors = this.factory.ngContentSelectors;
const projectableNodes = selectors.map(() => []);
const wildcardIdx = selectors.findIndex((selector) => selector === '*');

nodes.forEach((node) => {
  if (node instanceof Element) {
    // get element node that matches a select which is not the wildcard
    const projectedToIdx = selectors.findIndex(
      (selector, i) => i !== wildcardIdx && node.matches(selector)
    );

    if (projectedToIdx !== -1) {
      // add it to the corresponding projectableNodes and return
      projectableNodes[projectedToIdx].push(node);
      return;
    }
  } 

  if (wildcardIdx > -1) {
    // when there is a wildcard and it's not an Element or it cannot be,
    // matched to the selection up, add it to the global ng-content
    projectableNodes[wildcardIdx].push(node);
  }
});

console.log('projectableNodes', projectableNodes);

const embeddedComponent = this.factory.create(
  this.injector,
  projectableNodes,
  element
);

I guess this can be optimized, or perhaps there are some bugs in here, but I hope you'll get the idea. With logic like this, or something resembling this, you can have relatively dynamic components with dynamic ng-content

like image 113
Poul Kruijt Avatar answered Jan 21 '26 08:01

Poul Kruijt



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!