Angular2: renderuj komponent bez jego tagu opakowującego

106

Usiłuję znaleźć sposób, aby to zrobić. W komponencie nadrzędnym szablon opisuje a tablei jego theadelement, ale deleguje renderowanie tbodydo innego komponentu, na przykład:

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

Każdy składnik myResult renderuje swój własny trznacznik, zasadniczo w następujący sposób:

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

Powodem, dla którego nie umieszczam tego bezpośrednio w komponencie nadrzędnym (unikając potrzeby komponentu myResult), jest to, że składnik myResult jest w rzeczywistości bardziej skomplikowany niż pokazano tutaj, więc chcę umieścić jego zachowanie w oddzielnym komponencie i pliku.

Wynikowy DOM wygląda źle. Uważam, że dzieje się tak, ponieważ jest nieprawidłowy, ponieważ tbodymoże zawierać tylko trelementy (patrz MDN) , ale mój wygenerowany (uproszczony) DOM to:

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

Czy jest jakiś sposób, abyśmy mogli wyrenderować to samo, ale bez <my-result>tagu opakowującego i nadal używając komponentu, który jest wyłączną odpowiedzialnością za renderowanie wiersza tabeli?

Mam spojrzał na ng-content, DynamicComponentLoaderThe ViewContainerRef, ale nie wydaje się, aby zapewnić rozwiązanie tego ile widzę.

Greg
źródło
czy możesz pokazać działający przykład?
amina zakaria
2
Właściwa odpowiedź jest tam, z próbką plunkera stackoverflow.com/questions/46671235/ ...
sancelot
1
Żadna z proponowanych odpowiedzi nie działa ani nie jest kompletna. Właściwa odpowiedź jest opisana tutaj za pomocą przykładowego plunkera stackoverflow.com/questions/46671235/ ...
sancelot

Odpowiedzi:

101

Możesz użyć selektorów atrybutów

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

a następnie użyj go jak

<td myTd></td>
Günter Zöchbauer
źródło
Czy selektor twojego komponentu zawiera []? Czy dodałeś komponent do declarations: [...]modułu? Jeśli komponent jest zarejestrowany w innym module, czy dodałeś ten inny moduł do imports: [...]modułu, w którym chcesz użyć komponentu?
Günter Zöchbauer
1
Nie, setAttributeto nie jest mój kod. Ale rozgryzłem to, musiałem użyć rzeczywistego tagu najwyższego poziomu w moim szablonie jako tagu dla mojego komponentu, zamiast ng-containertak nowego, działającego użycia<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
Aviad P.,
1
Nie możesz ustawić komponentu atrybutu w ng-container, ponieważ jest on usuwany z DOM. Dlatego musisz ustawić go na rzeczywistym tagu HTML.
Robouste
2
@AviadP. Wielkie dzięki ... Wiedziałem, że potrzebna była drobna zmiana i traciłem rozum. Więc zamiast tego: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>użyłem tego (po zmianie li-navigation na selektor atrybutów)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
Mahesh
3
ta odpowiedź nie działa, jeśli próbujesz rozszerzyć komponent materiału, np. <mat-toolbar my-toolbar-rows> będzie narzekać, że możesz mieć tylko jeden selektor komponentu, a nie wiele. Mógłbym przejść do <my-toolbar-component>, ale wtedy umieszcza pasek narzędzi mat w div
robert king
25

Potrzebujesz „ViewContainerRef” i wewnątrz komponentu my-result wykonaj coś takiego:

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);
  }
Szczupły
źródło
5
Działa jak marzenie! Dziękuję Ci. Zamiast tego użyłem w Angular 8:@ViewChild('template', {static: true}) template;
AlainD.
2
To zadziałało. Miałem sytuację, w której korzystałem z wyświetlania css: grid i nie możesz nadal mieć tagu <my-result> jako pierwszego elementu w rodzicu ze wszystkimi elementami podrzędnymi renderowania my-result jako rodzeństwo do mojego wyniku . Problem polegał na tym, że jeden dodatkowy element widmo wciąż łamał siatki 7 kolumn "grid-template-columns: repeat (7, 1fr);" i renderował 8 elementów. 1 miejsce na ducha dla mojego wyniku i 7 nagłówków kolumn. Rozwiązaniem tego problemu było po prostu ukrycie go przez umieszczenie <my-result style = "display: none"> w tagu. Po tym system siatki CSS działał jak urok.
Randall Do
To rozwiązanie sprawdza się nawet w bardziej złożonym przypadku, w którym my-resulttrzeba mieć możliwość tworzenia nowego my-resultrodzeństwa. Więc wyobraź sobie, że masz hierarchię, w my-resultktórej każdy wiersz może mieć wiersze „potomne”. W takim przypadku użycie selektora atrybutu nie zadziałałoby, ponieważ pierwszy selektor znajduje się w elemencie tbody, ale drugi nie może przejść do wewnętrznego tbodyani do tr.
Daniel
1
Kiedy używam tego, jak mogę uniknąć uzyskania ExpressionChangedAfterItHasBeenCheckedError?
gyozo kudor
@gyozokudor prawdopodobnie używa setTimeout
Diego Fernando Murillo Valenci
12

Użyj tej dyrektywy na swoim elemencie

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

Przykładowe użycie:

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

aw nadrzędnym html wywołujesz element karty jak zwykle, na przykład:

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

Wynik będzie:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>
Shlomi Aharoni
źródło
6
Powoduje to zgłoszenie błędu, ponieważ parametr „parentElement.parentNode” ma wartość null
emirhosseini,
1
Pomysł jest dobry, ale nie działa poprawnie :-(
jpmottin
12

możesz spróbować użyć nowego css display: contents

oto mój scss paska narzędzi:

: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;
}

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

i w użyciu:

<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
Robert King
źródło
10

Selektory atrybutów to najlepszy sposób rozwiązania tego problemu.

Więc w twoim przypadku:

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

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

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

Zobacz działający stackblitz: https://stackblitz.com/edit/angular-xbbegx

Nicholas Murray
źródło
selector: 'my-results, [moje-wyniki]', wtedy mogę użyć moje-wyniki jako atrybut tagu w HTML. To jest poprawna odpowiedź. Działa w Angular 8.2.
Don Dilanga
4

Inną opcją w dzisiejszych czasach jest ContribNgHostModuleudostępnienie z @angular-contrib/commonpakietu .

Po zaimportowaniu modułu możesz dodać go host: { ngNoHost: '' }do @Componentdekoratora i żaden element opakowujący nie będzie renderowany.

mjswensen
źródło