Angular2: rendre un composant sans sa balise d'habillage

100

J'ai du mal à trouver un moyen de le faire. Dans un composant parent, le modèle décrit a tableet son theadélément, mais délègue le rendu du tbodyà un autre composant, comme ceci:

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

Chaque composant myResult rend sa propre trbalise, essentiellement comme ceci:

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

La raison pour laquelle je ne mets pas cela directement dans le composant parent (en évitant le besoin d'un composant myResult) est que le composant myResult est en fait plus compliqué que celui indiqué ici, donc je veux mettre son comportement dans un composant et un fichier séparés.

Le DOM résultant semble mauvais. Je pense que c'est parce qu'il est invalide, car tbodyil ne peut contenir que des tréléments (voir MDN) , mais mon DOM généré (simplifié) est:

<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-t-il un moyen d'obtenir le même rendu, mais sans la <my-result>balise d' emballage , et tout en utilisant un composant pour être seul responsable du rendu d'une ligne de table?

Je l' ai regardé ng-content, DynamicComponentLoaderla ViewContainerRef, mais ils ne semblent pas fournir une solution à ce pour autant que je peux voir.

Greg
la source
pouvez-vous s'il vous plaît montrer un exemple de travail?
zakaria amine
2
La bonne réponse est là, avec un exemple de plunker stackoverflow.com/questions/46671235/…
sancelot
1
Aucune des réponses proposées ne fonctionne ou n'est complète. La bonne réponse est décrite ici avec un exemple de plunker stackoverflow.com/questions/46671235/…
sancelot

Réponses:

97

Vous pouvez utiliser des sélecteurs d'attributs

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

puis utilisez-le comme

<td myTd></td>
Günter Zöchbauer
la source
Le sélecteur de votre composant contient-il le []? Avez-vous ajouté le composant à declarations: [...]un module? Si le composant est enregistré dans un module différent, avez-vous ajouté cet autre module au imports: [...]module où vous souhaitez utiliser le composant?
Günter Zöchbauer
1
Non, ce setAttributen'est pas mon code. Mais je l'ai compris, je devais utiliser la balise de premier niveau dans mon modèle comme balise pour mon composant au lieu de la ng-containernouvelle utilisation de travail est<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.
1
Vous ne pouvez pas définir le composant d'attribut sur ng-container car il est supprimé du DOM. C'est pourquoi vous devez le définir sur une balise HTML réelle.
Robouste
2
@AviadP. Merci beaucoup ... Je savais qu'il y avait un changement mineur qui était nécessaire et je perdais la tête à ce sujet. Donc au lieu de ceci: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>j'ai utilisé ceci (après avoir changé li-navigation en un sélecteur d'attribut)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
3
cette réponse ne fonctionne pas si vous essayez d'augmenter un composant matériel, par exemple <mat-toolbar my-toolbar-rows> se plaindra que vous ne pouvez avoir qu'un seul sélecteur de composant et non plusieurs. Je pourrais aller <my-toolbar-component> mais ensuite il met la barre d'outils mat dans un div
robert king
22

Vous avez besoin de "ViewContainerRef" et à l'intérieur du composant my-result, faites quelque chose comme ceci:

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);
  }
Svelte
la source
4
Fonctionne comme un charme! Je vous remercie. J'ai utilisé dans Angular 8 ce qui suit à la place:@ViewChild('template', {static: true}) template;
AlainD.
2
Cela a fonctionné. J'ai eu une situation dans laquelle j'utilisais l'affichage css: grid et vous ne pouvez pas avoir encore la balise <my-result> comme premier élément dans le parent avec tous les éléments enfants de my-result rendus en tant que frères et sœurs de my-result . Le problème était qu'un élément fantôme supplémentaire cassait encore les grilles de 7 colonnes "grid-template-columns: repeat (7, 1fr);" et il rendait 8 éléments. 1 espace réservé fantôme pour my-result et les 7 en-têtes de colonne. Le contournement pour cela était simplement de le cacher en mettant <my-result style = "display: none"> dans votre balise. Le système de grille css a fonctionné comme un charme après cela.
RandallJusqu'au
Cette solution fonctionne même dans un cas plus complexe, où il my-resultfaut pouvoir créer de nouveaux my-resultfrères et sœurs. Alors, imaginez que vous ayez une hiérarchie my-resultoù chaque ligne peut avoir des lignes "enfants". Dans ce cas, l'utilisation d'un sélecteur d'attribut ne fonctionnerait pas, car le premier sélecteur va dans le tbody, mais le second ne peut pas aller dans un interne tbodyni dans un tr.
Daniel
1
Quand j'utilise ceci, comment puis-je éviter d'avoir ExpressionChangedAfterItHasBeenCheckedError?
gyozo kudor le
@gyozokudor peut-être en utilisant un setTimeout
Diego Fernando Murillo Valenci
12

Utilisez cette directive sur votre élément

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

Exemple d'utilisation:

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

et dans le html parent, vous appelez l'élément card comme d'habitude, par exemple:

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

La sortie sera:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Shlomi Aharoni
la source
6
Cela génère une erreur car 'parentElement.parentNode' est nul
emirhosseini
1
L'idée est bonne mais cela ne fonctionne pas correctement :-(
jpmottin
9

Les sélecteurs d'attributs sont le meilleur moyen de résoudre ce problème.

Donc dans votre cas:

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

mes-résultats 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() {
  }

}

mes-résultats html

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

mon-résultat 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() {
  }

}

my-result html

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

Voir stackblitz de travail: https://stackblitz.com/edit/angular-xbbegx

Nicholas Murray
la source
selector: 'mes-résultats, [mes-résultats]', alors je peux utiliser mes-résultats comme attribut de la balise en HTML. C'est la bonne réponse. Cela fonctionne dans Angular 8.2.
Don Dilanga le
8

vous pouvez essayer d'utiliser le nouveau css display: contents

voici ma barre d'outils 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;
}

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

et en cours d'utilisation:

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

Une autre option de nos jours est la ContribNgHostModulemise à disposition à partir du @angular-contrib/commonpackage .

Après avoir importé le module, vous pouvez l'ajouter host: { ngNoHost: '' }à votre @Componentdécorateur et aucun élément d'emballage ne sera rendu.

mjswensen
la source