Angular2: renderiza un componente sin su etiqueta de envoltura

100

Estoy luchando por encontrar una manera de hacer esto. En un componente principal, la plantilla describe a tabley su theadelemento, pero delega la representación tbodya otro 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 genera su propia tretiqueta, básicamente así:

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

La razón por la que no pongo esto directamente en el componente principal (evitando la necesidad de un componente myResult) es que el componente myResult es en realidad más complicado de lo que se muestra aquí, por lo que quiero poner su comportamiento en un componente y archivo separados.

El DOM resultante se ve mal. Creo que esto se debe a que no es válido, ya tbodyque solo puede contener trelementos (ver MDN) , pero mi DOM generado (simplificado) es:

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

¿Hay alguna forma de que podamos obtener lo mismo renderizado, pero sin la <my-result>etiqueta de envoltura , y mientras todavía usamos un componente para ser el único responsable de renderizar una fila de la tabla?

He mirado ng-content, DynamicComponentLoaderel ViewContainerRef, pero no parecen proporcionar una solución a este por lo que yo puedo ver.

Greg
fuente
¿Puede mostrar un ejemplo práctico?
amina zakaria
2
La respuesta correcta está ahí, con una muestra de plunker stackoverflow.com/questions/46671235/…
sancelot
1
Ninguna de las respuestas propuestas funciona o está completa. La respuesta correcta se describe aquí con una muestra de plunker stackoverflow.com/questions/46671235/…
sancelot

Respuestas:

98

Puede utilizar selectores de atributos

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

y luego úsalo como

<td myTd></td>
Günter Zöchbauer
fuente
¿El selector de su componente contiene el []? ¿Agregó el componente a declarations: [...]un módulo? Si el componente está registrado en un módulo diferente, ¿agregó este otro módulo al imports: [...]módulo donde desea utilizar el componente?
Günter Zöchbauer
1
No, setAttributeno es mi código. Pero lo he descubierto, necesitaba usar la etiqueta de nivel superior real en mi plantilla como la etiqueta para mi componente en lugar de un ng-containernuevo uso de trabajo<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.
1
No puede establecer el componente de atributo en ng-container porque se eliminó del DOM. Es por eso que debe configurarlo en una etiqueta HTML real.
Robouste
2
@AviadP. Muchas gracias ... Sabía que se requería un cambio menor y estaba perdiendo la cabeza por esto. Entonces, en lugar de esto: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>utilicé esto (después de cambiar li-navigation a un selector de atributos)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
3
esta respuesta no funciona si está tratando de aumentar un componente de material, por ejemplo, <mat-toolbar my-toolbar-rows> se quejará de que solo puede tener un selector de componente, no varios. Podría ir a <my-toolbar-component> pero luego está colocando la barra de herramientas mat dentro de un div
robert king
22

Necesita "ViewContainerRef" y dentro del componente my-result haga algo como esto:

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);
  }
Delgado
fuente
4
¡Funciona de maravilla! Gracias. @ViewChild('template', {static: true}) template;
Usé
2
Esto funcionó. Tuve una situación en la que estaba usando css display: grid y no puede tener todavía la etiqueta <my-result> como primer elemento en el padre con todos los elementos secundarios de my-result como hermanos de my-result . El problema era que un elemento fantasma adicional aún rompía las cuadrículas de 7 columnas "grid-template-columnas: repeat (7, 1fr);" y estaba renderizando 8 elementos. 1 marcador de posición fantasma para my-result y los 7 encabezados de columna. La solución para esto fue simplemente ocultarlo poniendo <my-result style = "display: none"> en su etiqueta. El sistema de cuadrícula css funcionó a la perfección después de eso.
Randall al
Esta solución funciona incluso en un caso más complejo, donde my-resultes necesario poder crear nuevos my-resulthermanos. Entonces, imagina que tienes una jerarquía my-resulten la que cada fila puede tener filas "secundarias". En ese caso, el uso de un selector de atributos no funcionaría, ya que el primer selector va en el tbody, pero el segundo no puede ir en un interno tbodyni en un tr.
Daniel
1
Cuando uso esto, ¿cómo puedo evitar obtener ExpressionChangedAfterItHasBeenCheckedError?
gyozo kudor
@gyozokudor tal vez usando un setTimeout
Diego Fernando Murillo Valenci
12

Utilice esta directiva en su 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);
   }
}

Uso de ejemplo:

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

y en el html principal, llama al elemento de tarjeta como de costumbre, por ejemplo:

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

La salida será:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Shlomi Aharoni
fuente
6
Esto arroja un error porque 'parentElement.parentNode' es nulo
emirhosseini
1
La idea es buena pero no funciona correctamente :-(
jpmottin
9

Los selectores de atributos son la mejor manera de resolver este problema.

Entonces, en tu caso:

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

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

}

mis-resultados html

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

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

}

html de mi resultado

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

Ver stackblitz de trabajo: https://stackblitz.com/edit/angular-xbbegx

Nicolás Murray
fuente
selector: 'mis-resultados, [mis-resultados]', entonces puedo usar mis-resultados como un atributo de la etiqueta en HTML. Esta es la respuesta correcta. Funciona en Angular 8.2.
Don Dilanga
9

puedes intentar usar el nuevo css display: contents

aquí está mi barra de herramientas 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;
}

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

y en uso:

<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
Robert King
fuente
3

Otra opción hoy en día es la ContribNgHostModulepuesta a disposición del @angular-contrib/commonpaquete .

Después de importar el módulo, puede agregarlo host: { ngNoHost: '' }a su @Componentdecorador y no se procesará ningún elemento de envoltura.

mjswensen
fuente