Angular2: рендеринг компонента без тега оболочки

107

Я изо всех сил пытаюсь найти способ сделать это. В родительском компоненте шаблон описывает a tableи его theadэлемент, но делегирует отрисовку tbodyдругому компоненту, например:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Time</th>
    </tr>
  </thead>
  <tbody *ngFor="let entry of getEntries()">
    <my-result [entry]="entry"></my-result>
  </tbody>
</table>

Каждый компонент myResult отображает свой собственный trтег, примерно так:

<tr>
  <td>{{ entry.name }}</td>
  <td>{{ entry.time }}</td>
</tr>

Причина, по которой я не помещаю это непосредственно в родительский компонент (избегая необходимости в компоненте myResult), заключается в том, что компонент myResult на самом деле сложнее, чем показано здесь, поэтому я хочу поместить его поведение в отдельный компонент и файл.

В результате DOM выглядит плохо. Я считаю, что это потому, что он недействителен, так как tbodyможет содержать только trэлементы (см. MDN) , но моя сгенерированная (упрощенная) DOM:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Time</th>
    </tr>
  </thead>
  <tbody>
    <my-result>
      <tr>
        <td>Bob</td>
        <td>128</td>
      </tr>
    </my-result>
  </tbody>
  <tbody>
    <my-result>
      <tr>
        <td>Lisa</td>
        <td>333</td>
      </tr>
    </my-result>
  </tbody>
</table>

Есть ли способ получить то же самое, но без <my-result>тега обертывания и при этом использовать компонент, который будет нести единоличную ответственность за визуализацию строки таблицы?

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

Грег
источник
не могли бы вы показать рабочий пример?
zakaria amine
2
Правильный ответ есть, с образцом плунжера stackoverflow.com/questions/46671235/…
sancelot
1
Ни один из предложенных ответов не работает или является полным. Правильный ответ описан здесь с образцом плунжера stackoverflow.com/questions/46671235/…
sancelot

Ответы:

101

Вы можете использовать селекторы атрибутов

@Component({
  selector: '[myTd]'
  ...
})

а затем используйте его как

<td myTd></td>
Гюнтер Цёхбауэр
источник
Содержит ли селектор вашего компонента символ []? Вы добавляли компонент в declarations: [...]модуль? Если компонент зарегистрирован в другом модуле, добавляли ли вы этот другой модуль imports: [...]в модуль, в котором вы хотите использовать компонент?
Günter Zöchbauer
1
Нет, setAttributeэто не мой код. Но я понял это, мне нужно было использовать фактический тег верхнего уровня в моем шаблоне в качестве тега для моего компонента вместо ng-containerнового рабочего использования<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.
1
Вы не можете установить компонент атрибута в ng-контейнере, потому что он удален из DOM. Вот почему вам нужно установить его в реальном теге HTML.
Робуст
2
@AviadP. Большое спасибо ... Я знал, что требуется небольшое изменение, и терял рассудок из-за этого. Итак, вместо этого: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>я использовал это (после изменения li-navigation на селектор атрибутов)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
4
этот ответ не работает, если вы пытаетесь расширить компонент материала, например, <mat-toolbar my-toolbar-rows> будет жаловаться, что у вас может быть только один селектор компонентов, а не несколько. Я мог бы пойти <my-toolbar-component>, но тогда он помещает панель инструментов mat в div
Роберт
25

Вам нужен ViewContainerRef, и внутри компонента my-result сделайте что-то вроде этого:

html:

<ng-template #template>
    <tr>
       <td>Lisa</td>
       <td>333</td>
    </tr>
 </ng-template>

ts:

@ViewChild('template') template;


  constructor(
    private viewContainerRef: ViewContainerRef
  ) { }

  ngOnInit() {
    this.viewContainerRef.createEmbeddedView(this.template);
  }
Тонкий
источник
6
Работает как шарм! Спасибо. Вместо этого я использовал в Angular 8 следующее:@ViewChild('template', {static: true}) template;
AlainD.
2
Это сработало. У меня была ситуация, когда я использовал css display: grid, и у вас не может быть, чтобы он все еще имел тег <my-result> в качестве первого элемента в родительском элементе со всеми дочерними элементами my-result, отображаемыми как братья и сестры для my-result . Проблема заключалась в том, что один лишний призрачный элемент все еще нарушал сетку 7 столбцов «grid-template-columns: repeat (7, 1fr);» и он рендерил 8 элементов. 1 место-призрак для my-result и 7 заголовков столбцов. Для этого нужно было просто скрыть это, поместив <my-result style = "display: none"> в свой тег. После этого сетка CSS работала как шарм.
Randall до
Это решение работает даже в более сложном случае, когда my-resultнеобходимо иметь возможность создавать новых my-resultбратьев и сестер. Итак, представьте, что у вас есть иерархия, в my-resultкоторой каждая строка может иметь «дочерние» строки. В этом случае использование селектора атрибутов не сработает, поскольку первый селектор входит в tbody, а второй не может входить tbodyни во внутренний, ни в tr.
Даниэль
1
Когда я использую это, как я могу избежать ExpressionChangedAfterItHasBeenCheckedError?
gyozo kudor
@gyozokudor, возможно, использует setTimeout
Диего Фернандо Мурильо Валенси
13

вы можете попробовать использовать новый CSS display: contents

вот моя панель инструментов scss:

:host {
  display: contents;
}

:host-context(.is-mobile) .toolbar {
  position: fixed;
  /* Make sure the toolbar will stay on top of the content as it scrolls past. */
  z-index: 2;
}

h1.app-name {
  margin-left: 8px;
}

и html:

<mat-toolbar color="primary" class="toolbar">
  <button mat-icon-button (click)="toggle.emit()">
    <mat-icon>menu</mat-icon>
  </button>
  <img src="/assets/icons/favicon.png">
  <h1 class="app-name">@robertking Dashboard</h1>
</mat-toolbar>

и в использовании:

<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
Роберт Кинг
источник
12

Используйте эту директиву в своем элементе

@Directive({
   selector: '[remove-wrapper]'
})
export class RemoveWrapperDirective {
   constructor(private el: ElementRef) {
       const parentElement = el.nativeElement.parentElement;
       const element = el.nativeElement;
       parentElement.removeChild(element);
       parentElement.parentNode.insertBefore(element, parentElement.nextSibling);
       parentElement.parentNode.removeChild(parentElement);
   }
}

Пример использования:

<div class="card" remove-wrapper>
   This is my card component
</div>

а в родительском html вы вызываете элемент карты как обычно, например:

<div class="cards-container">
   <card></card>
</div>

Результат будет:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Шломи Ахарони
источник
6
Это вызывает ошибку, потому что parentElement.parentNode имеет значение null
emirhosseini
1
Идея хороша, но работает некорректно :-(
jpmottin
10

Селекторы атрибутов - лучший способ решить эту проблему.

Итак, в вашем случае:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Time</th>
    </tr>
  </thead>
  <tbody my-results>
  </tbody>
</table>

мои результаты ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'my-results, [my-results]',
  templateUrl: './my-results.component.html',
  styleUrls: ['./my-results.component.css']
})
export class MyResultsComponent implements OnInit {

  entries: Array<any> = [
    { name: 'Entry One', time: '10:00'},
    { name: 'Entry Two', time: '10:05 '},
    { name: 'Entry Three', time: '10:10'},
  ];

  constructor() { }

  ngOnInit() {
  }

}

мои результаты html

  <tr my-result [entry]="entry" *ngFor="let entry of entries"><tr>

мой-результат ts

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: '[my-result]',
  templateUrl: './my-result.component.html',
  styleUrls: ['./my-result.component.css']
})
export class MyResultComponent implements OnInit {

  @Input() entry: any;

  constructor() { }

  ngOnInit() {
  }

}

мой-результат html

  <td>{{ entry.name }}</td>
  <td>{{ entry.time }}</td>

См. Рабочий stackblitz: https://stackblitz.com/edit/angular-xbbegx

Николас Мюррей
источник
selector: 'my-results, [my-results]', тогда я могу использовать my-results как атрибут тега в HTML. Это правильный ответ. Он работает в Angular 8.2.
Дон Диланга
5

Другой вариант в настоящее время - ContribNgHostModuleдоступный из @angular-contrib/commonпакета .

После импорта модуля вы можете добавить его host: { ngNoHost: '' }в свой @Componentдекоратор, и никакие элементы упаковки не будут отображаться.

mjswensen
источник