Angular2: renderiza um componente sem sua tag de envolvimento

100

Estou lutando para encontrar uma maneira de fazer isso. Em um componente pai, o modelo descreve um tablee seu theadelemento, mas delega a renderização de tbodypara outro componente, como este:

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

Cada componente myResult renderiza sua própria trtag, basicamente assim:

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

O motivo pelo qual não estou colocando isso diretamente no componente pai (evitando a necessidade de um componente myResult) é que o componente myResult é, na verdade, mais complicado do que o mostrado aqui, portanto, quero colocar seu comportamento em um componente e arquivo separados.

O DOM resultante parece ruim. Acredito que seja porque ele é inválido, pois tbodysó pode conter trelementos (consulte MDN) , mas meu DOM gerado (simplificado) é:

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

Existe alguma maneira de obtermos a mesma coisa renderizada, mas sem a <my-result>marca de empacotamento , e ainda usando um componente para ser o único responsável por renderizar uma linha da tabela?

Eu olhei ng-content, DynamicComponentLoader, o ViewContainerRef, mas eles não parecem fornecer uma solução para este, tanto quanto eu posso ver.

Greg
fonte
você pode mostrar um exemplo de trabalho?
zakaria amina
2
A resposta certa está lá, com um exemplo de êmbolo stackoverflow.com/questions/46671235/…
sancelot
1
Nenhuma das respostas propostas está funcionando ou está completa. A resposta certa é descrita aqui com um exemplo de êmbolo stackoverflow.com/questions/46671235/…
sancelot

Respostas:

95

Você pode usar seletores de atributo

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

e então usá-lo como

<td myTd></td>
Günter Zöchbauer
fonte
O seletor do seu componente contém o []? Você adicionou o componente declarations: [...]de um módulo? Se o componente está registrado em um módulo diferente, você adicionou este outro módulo ao imports: [...]do módulo onde deseja usar o componente?
Günter Zöchbauer
1
Não, setAttributenão é meu código. Mas eu descobri, eu precisava usar a marca de nível superior real em meu modelo como a marca para meu componente em vez de ng-containerusar o novo uso<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.
1
Você não pode definir o componente de atributo no ng-container porque ele é removido do DOM. É por isso que você precisa configurá-lo em uma tag HTML real.
Robouste
2
@AviadP. Muito obrigado ... Eu sabia que era necessária uma pequena mudança e estava perdendo a cabeça por causa disso. Então, em vez disso: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>Eu usei isto (depois de alterar a navegação para um seletor de atributo)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
3
esta resposta não funciona se você está tentando aumentar um componente de material, por exemplo, <mat-toolbar my-toolbar-rows> reclamará que você só pode ter um seletor de componente, não vários. Eu poderia ir <my-toolbar-component>, mas ele está colocando a barra de ferramentas da esteira dentro de uma div
robert king
20

Você precisa de "ViewContainerRef" e dentro do componente my-result faça algo assim:

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);
  }
Fino
fonte
3
Funciona como um encanto! Obrigado. Em vez disso, usei no Angular 8 o seguinte:@ViewChild('template', {static: true}) template;
AlainD.
1
Isso funcionou. Eu tive uma situação em que estava usando css display: grid e você não pode fazer com que ele ainda tenha a tag <my-result> como o primeiro elemento no pai com todos os elementos filhos de my-result dividindo como irmãos de my-result . O problema era que um elemento fantasma extra ainda quebrava as 7 colunas das grades "grid-template-columns: repeat (7, 1fr);" e estava renderizando 8 elementos. 1 espaço reservado fantasma para meu resultado e os cabeçalhos de 7 colunas. A solução para isso foi simplesmente ocultá-lo, colocando <my-result style = "display: none"> em sua tag. O sistema de grade css funcionou como um encanto depois disso.
RandallTo
Esta solução funciona mesmo em um caso mais complexo, onde my-resultprecisa ser capaz de criar novos my-resultirmãos. Então, imagine que você tem uma hierarquia de my-resultonde cada linha pode ter linhas "filhas". Nesse caso, usar um seletor de atributo não funcionaria, pois o primeiro seletor vai no tbody, mas o segundo não pode ir em um interno tbodynem em a tr.
Daniel
1
Quando eu uso isso, como posso evitar a obtenção de ExpressionChangedAfterItHasBeenCheckedError?
gyozo kudor
@gyozokudor talvez usando um setTimeout
Diego Fernando Murillo Valenci
12

Use esta diretiva em seu elemento

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

Exemplo de uso:

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

e no HTML pai você chama o elemento cartão como de costume, por exemplo:

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

O resultado será:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Shlomi Aharoni
fonte
6
Isso está gerando um erro porque 'parentElement.parentNode' é nulo
emirhosseini
A ideia é boa, mas não está funcionando corretamente :-(
jpmottin
9

Os seletores de atributos são a melhor maneira de resolver esse problema.

Então, no seu caso:

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

meus resultados 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() {
  }

}

meus-resultados html

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

meu-resultado 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() {
  }

}

meu-resultado html

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

Veja stackblitz de trabalho: https://stackblitz.com/edit/angular-xbbegx

Nicholas Murray
fonte
seletor: 'my-results, [my-results]', então posso usar my-results como um atributo da tag em HTML. Essa é a resposta correta. Ele funciona no Angular 8.2.
Don Dilanga
8

você pode tentar usar o novo css display: contents

aqui está minha barra de ferramentas 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;
}

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

e em uso:

<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
robert king
fonte
3

Outra opção hoje em dia é o ContribNgHostModuledisponibilizado na @angular-contrib/commonembalagem .

Depois de importar o módulo, você pode adicioná-lo host: { ngNoHost: '' }ao seu @Componentdecorador e nenhum elemento de embalagem será renderizado.

mjswensen
fonte