Динамическая компиляция компонентов Angular – ng-контент с выбором не работает

У меня есть некоторые компоненты, которые компилируются динамически с использованием resolveComponentFactory.

Я делаю это так....

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,
);

Но у меня есть проблема, которую я пытаюсь решить....

Это работает так, как ожидалось

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

Это НЕ работает должным образом:

<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>

Кажется, что атрибут выбора ng-content не работает с динамически скомпилированным содержимым.

Любые идеи, что я делаю неправильно или это ошибка?

Я использую Angular 8, если это поможет.

Обновление: я воспроизвел эту проблему в stackBlitz здесь: https://stackblitz.com/edit/angular-ivy-givxx6


person Ewan    schedule 14.05.2020    source источник
comment
Можете ли вы добавить stackblitz? Это сделало бы гораздо более ясным, чего вы пытаетесь достичь   -  person Poul Kruijt    schedule 14.05.2020
comment
Я воссоздал эту проблему здесь stackblitz.com/edit/angular-ivy-givxx6 на стекблиц.   -  person Ewan    schedule 19.05.2020
comment
Привет, мир динамически генерируется из строки, но ‹ng-content select=title›.. не работает   -  person Ewan    schedule 19.05.2020


Ответы (2)


Я еще не разобрался с параметром проецируемых узлов в сочетании с ng-select.

Но если вы посмотрите мой stackblitz вы можете видеть, как это работает .. вроде.

Что я узнал, что происходит:

  1. ng-select обрабатываются в перевернутом виде (я так думаю). Это означает, что он сначала обрабатывает последний выбор в шаблоне и проверяет последний массив в массиве 2-мерных проецируемых узлов
  2. Когда вы указываете запрос на выборку, и он не может найти его в соответствующем массиве, он просто показывает все элементы в массиве
  3. Первый ng-select соответствует первому массиву в массиве 2d.
  4. Если у вас есть узел, который находится в запросе выбора, а также добавить его в следующий массив, он не будет найден в первом выборе и по какой-то причине будет добавлен в следующий. Наоборот - не проблема.

Итак, если у вас есть это как динамический 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>

Вы должны сделать свои узлы следующими

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

Чтобы соответствовать такому шаблону:

<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>

Чтобы объяснить пункт 4 (и, возможно, пункт 1), если вы это сделаете:

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

Нет проблем. Если сделать наоборот:

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

Он никогда не будет проецировать узлы [1] в правильном положении, но каким-то образом будет добавлен ко второму <ng-content> с «другим заголовком». Я нашел этот ответ, который имеет несколько голосов, но мне не очень понятно, как работает механизм , или каким образом вы сможете сделать это динамичным.

Я надеюсь, что это даст вам некоторое представление, и, возможно, вы сможете придумать динамическое решение и опубликовать свой собственный ответ. Или, возможно, это была вся информация, которая вам нужна, и я действительно помог вам :D кто знает


Заглянув немного в исходный код, вы можете узнать количество ng-content, которое у вас есть в вашем элементе. Это определяется factory.ngContentSelectors. В моем stackblitz это:

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

Затем вы можете использовать метод Element.matches(), чтобы выбрать этот содержимое, а остальное передать подстановочному знаку. Затем вы можете сделать его динамическим, используя следующую логику:

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
);

Я предполагаю, что это можно оптимизировать, или, возможно, здесь есть какие-то ошибки, но я надеюсь, что вы поняли идею. С такой логикой или чем-то подобным вы можете иметь относительно динамические компоненты с динамическими ng-content

person Poul Kruijt    schedule 19.05.2020
comment
Спасибо, у меня все заработало с вашей помощью. Я написал функцию для получения ngContent с помощью factory.ngSelectors. - person Ewan; 21.05.2020

Спасибо Пол. Ваш ответ мне очень помог. Я написал ниже, и мои модульные тесты подтверждают, что теперь все работает так, как ожидалось.

const projectableNodes = this.getNgContent( element, factory.ngContentSelectors );


private getNgContent( element: Element, ngSelectors: string[] ) {

    // select the content
    const elements = ngSelectors
      .map ( s => {
        if ( s !== '*' ) {
          const els = element.querySelectorAll( s );
          els.forEach( e => e.remove() );
          return Array.from( els );
        }
        return s;
      });

    // where content has not been selected return remaining
    return elements.map( s => {
      if ( s === '*' ) {
        return Array.from( element.childNodes )
      }
      return s;
    });
  }
person Ewan    schedule 21.05.2020