Angular2: Rendern einer Komponente ohne das Wrapping-Tag

100

Ich kämpfe darum, einen Weg zu finden, dies zu tun. In einer übergeordneten Komponente beschreibt die Vorlage ein tableund sein theadElement, delegiert das Rendern jedoch tbodyan eine andere Komponente wie folgt:

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

Jede myResult-Komponente rendert im trGrunde genommen ein eigenes Tag:

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

Der Grund, warum ich dies nicht direkt in die übergeordnete Komponente einfüge (ohne dass eine myResult-Komponente erforderlich ist), ist, dass die myResult-Komponente tatsächlich komplizierter ist als hier gezeigt. Daher möchte ich ihr Verhalten in eine separate Komponente und Datei einfügen.

Das resultierende DOM sieht schlecht aus. Ich glaube, das liegt daran, dass es ungültig ist, da tbodyes nur trElemente enthalten kann (siehe MDN) , aber mein generiertes (vereinfachtes) DOM ist:

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

Gibt es eine Möglichkeit, dasselbe zu rendern, aber ohne die Umhüllung? <my-result> zu rendern Tag, und während weiterhin eine Komponente verwenden, um allein für das Rendern einer Tabellenzeile verantwortlich zu sein?

Ich habe betrachtet ng-content, DynamicComponentLoaderdie ViewContainerRef, aber sie scheinen nicht , eine Lösung für diese zu schaffen , soweit ich sehen kann.

Greg
quelle
Können Sie bitte ein funktionierendes Beispiel zeigen?
Zakaria Amin
2
Die richtige Antwort ist da, mit einem Plunker-Beispiel stackoverflow.com/questions/46671235/…
sancelot
1
Keine der vorgeschlagenen Antworten funktioniert oder ist vollständig. Die richtige Antwort wird hier mit einem Plunker-Beispiel stackoverflow.com/questions/46671235/…
sancelot

Antworten:

93

Sie können Attributselektoren verwenden

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

und dann benutze es gerne

<td myTd></td>
Günter Zöchbauer
quelle
Enthält der Selektor Ihrer Komponente das []? Haben Sie die Komponente declarations: [...]eines Moduls hinzugefügt ? Wenn die Komponente in einem anderen Modul registriert ist, haben Sie dieses andere Modul zu imports: [...]dem Modul hinzugefügt, in dem Sie die Komponente verwenden möchten?
Günter Zöchbauer
1
Nein setAttributeist nicht mein Code. Aber ich habe es herausgefunden, ich musste das eigentliche Tag der obersten Ebene in meiner Vorlage als Tag für meine Komponente verwenden, anstatt ng-containerso neu zu arbeiten<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.
1
Sie können die Attributkomponente für ng-container nicht festlegen, da sie aus dem DOM entfernt wird. Aus diesem Grund müssen Sie es auf ein tatsächliches HTML-Tag setzen.
Robouste
2
@AviadP. Vielen Dank ... Ich wusste, dass eine kleine Änderung erforderlich war und ich verlor meinen Verstand darüber. Also stattdessen: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>Ich habe dies verwendet (nachdem ich die Navigation in einen Attributselektor geändert hatte)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
3
Diese Antwort funktioniert nicht, wenn Sie versuchen, eine Materialkomponente zu erweitern, z. B. <mat-toolbar my-toolbar-rows> beschwert sich, dass Sie nur einen Komponenten-Selektor haben können, nicht mehrere. Ich könnte <my-toolbar-component> gehen, aber dann wird die Mat-Symbolleiste in einen div
robert king
20

Sie benötigen "ViewContainerRef" und tun in meiner Ergebniskomponente Folgendes:

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);
  }
Schlank
quelle
3
Klappt wunderbar! Danke dir. Ich habe stattdessen in Angular 8 Folgendes verwendet:@ViewChild('template', {static: true}) template;
AlainD.
1
Das hat funktioniert. Ich hatte eine Situation, in der ich css display: grid verwendet habe, und Sie können nicht haben, dass das <my-result> -Tag immer noch das erste Element im übergeordneten Element ist, wobei alle untergeordneten Elemente meines Ergebnisses als Geschwister zu meinem Ergebnis gerendert werden . Das Problem war, dass ein zusätzliches Geisterelement immer noch die Gitter 7 Spalten "Gitter-Vorlagen-Spalten: Wiederholung (7, 1fr);" und es wurden 8 Elemente gerendert. 1 Geisterplatzhalter für mein Ergebnis und die 7 Spaltenüberschriften. Die Lösung bestand darin, es einfach auszublenden, indem Sie <my-result style = "display: none"> in Ihr Tag einfügen. Das CSS-Rastersystem funktionierte danach wie ein Zauber.
RandallTo
Diese Lösung funktioniert auch in komplexeren Fällen, in denen my-resultneue my-resultGeschwister erstellt werden müssen. Stellen Sie sich also vor, Sie haben eine Hierarchie, in der my-resultjede Zeile "untergeordnete" Zeilen haben kann. In diesem Fall würde die Verwendung eines Attributselektors nicht funktionieren, da der erste Selektor in den geht tbody, der zweite jedoch weder in einen internen tbodynoch in einen tr.
Daniel
1
Wie kann ich vermeiden, ExpressionChangedAfterItHasBeenCheckedError zu erhalten, wenn ich dies verwende?
Gyozo Kudor
@gyozokudor vielleicht mit einem setTimeout
Diego Fernando Murillo Valenci
12

Verwenden Sie diese Anweisung für Ihr Element

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

Anwendungsbeispiel:

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

und im übergeordneten HTML rufen Sie das Kartenelement wie gewohnt auf, zum Beispiel:

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

Die Ausgabe wird sein:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Shlomi Aharoni
quelle
6
Dies löst einen Fehler aus, da 'parentElement.parentNode' null ist
emirhosseini
Die Idee ist gut, aber es funktioniert nicht richtig :-(
jpmottin
9

Attributselektoren sind der beste Weg, um dieses Problem zu lösen.

Also in deinem Fall:

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

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

}

my-results html

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

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

}

Mein Ergebnis HTML

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

Siehe Stackblitz: https://stackblitz.com/edit/angular-xbbegx

Nicholas Murray
quelle
Selektor: 'meine Ergebnisse, [meine Ergebnisse]', dann kann ich meine Ergebnisse als Attribut des Tags in HTML verwenden. Dies ist die richtige Antwort. Es funktioniert in Angular 8.2.
Don Dilanga
8

Sie können versuchen, das neue CSS zu verwenden display: contents

Hier ist meine Symbolleiste 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;
}

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

und in Gebrauch:

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

Eine weitere Option ist heutzutage die ContribNgHostModuleaus dem Paket zur Verfügung gestellte@angular-contrib/common .

Nach dem Import des Moduls können Sie host: { ngNoHost: '' }es Ihrem @ComponentDekorateur hinzufügen und es wird kein Wrapping-Element gerendert.

mjswensen
quelle