ChatGPT解决这个技术问题 Extra ChatGPT

Angular2 : render a component without its wrapping tag

I am struggling to find a way to do this. In a parent component, the template describes a table and its thead element, but delegates rendering the tbody to another component, like this:

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

Each myResult component renders its own tr tag, basically like so:

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

The reason I'm not putting this directly in the parent component (avoiding the need for a myResult component) is that the myResult component is actually more complicated than shown here, so I want to put its behaviour in a separate component and file.

The resulting DOM looks bad. I believe this is because it is invalid, as tbody can only contain tr elements (see MDN), but my generated (simplified) DOM is :

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

Is there any way we can get the same thing rendered, but without the wrapping <my-result> tag, and while still using a component to be sole responsible for rendering a table row ?

I have looked at ng-content, DynamicComponentLoader, the ViewContainerRef, but they don't seem to provide a solution to this as far as I can see.

can you please show a working example ?
The right answer is there, with a plunker sample stackoverflow.com/questions/46671235/…
None of the proposed answer are working, or are complete. The right answer is described here with a plunker sample stackoverflow.com/questions/46671235/…

G
Günter Zöchbauer

You can use attribute selectors

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

and then use it like

<td myTd></td>

No setAttribute is not my code. But I've figured it out, I needed to use the actual top level tag in my template as the tag for my component instead of ng-container so new working usage is <ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
You can't set attribute component on ng-container because it is removed from the DOM. That's why you need to set it on an actual HTML tag.
@AviadP. Thanks much... I knew there was a minor change that was required and I was losing my mind over this. So instead of this: <ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul> I used this (after changing li-navigation to an attribute selector) <ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
this answer doesn't work if you're trying to augment a material compoent e.g. will complain that you can only have one component selector not multiple. I could go but then it's putting the mat-toolbar inside a div
This solution works for simple cases (like OP's), but if you need to create a my-results that inside can spawn more my-results, then you need ViewContainerRef (check @Slim's answer below). The reason this solution doesn't work in that case is that the first attribute selector goes in a tbody, but where would the internal selector be? It can't be in a tr and you can't put a tbody inside of another tbody.
G
G. Modebadze

You need "ViewContainerRef" and inside my-result component do something like this:

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

Works like a charm! Thank you. I used in Angular 8 the following instead: @ViewChild('template', {static: true}) template;
This worked. I had a situation i was using css display:grid and you can't have it still have the tag as first element in the parent with all of the child elements of my-result rending as siblings to my-result. The problem was that one extra ghost element still broke the grids 7 columns "grid-template-columns:repeat(7, 1fr);" and it was rendering 8 elements. 1 ghost place holder for my-result and the 7 column headers. The work around for this was to simply hide it by putting in your tag. The css grid system worked like a charm after that.
This solution works even in a more complex case, where my-result needs to be able to create new my-result siblings. So, imagine you have a hierarchy of my-result where each row can have "children" rows. In that case, using an attribute selector wouldn't work, as the first selector goes in the tbody, but the second can't go in an internal tbody nor in a tr.
When I use this how can I avoid getting ExpressionChangedAfterItHasBeenCheckedError?
If anyone has errors with this.viewContainerRef, move code from ngOnInit to ngAfterViewInit.
R
Rusty Rob

you can try use the new css display: contents

here's my toolbar 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;
}

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

and in use:

<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>

display: contents does the job. This is what I was looking for, it allows the child component to take its style without being hacked by extra child component's name tag(selector)
N
Nicholas Murray

Attribute selectors are the best way to solve this issue.

So in your case:

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

See working stackblitz: https://stackblitz.com/edit/angular-xbbegx


selector: 'my-results, [my-results]', then I can use my-results as an attribute of the tag in HTML. This is the correct answer. It works in Angular 8.2.
S
Shlomi Aharoni

Use this directive on your 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);
   }
}

Example usage:

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

and in the parent html you call card element as usual, for example:

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

The output will be:

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

This is throwing an error because 'parentElement.parentNode' is null
Won't this mess with Angular's change detection and virtual DOM?
@BenWinding it won't because Angular internally represents a component using a data structure commonly referred to as a View or a Component View and all change detection operations, including ViewChildren run on a View, not the DOM. I have tested this code and I can confirm that all DOM listeners and angular bindings are working.
this solution doesn't work, throws null and destroys the ng structure
This removes the component altogether
m
mjswensen

Another option nowadays is the ContribNgHostModule made available from the @angular-contrib/common package.

After importing the module you can add host: { ngNoHost: '' } to your @Component decorator and no wrapping element will be rendered.


Just commenting because it looks like this package is not maintained anymore and there are errors with Angular 9+
A
Ali Almutawakel

Improvement on @Shlomi Aharoni answer. It is generally good practice to use Renderer2 to manipulate the DOM to keep Angular in the loop and because for other reasons including security (e.g. XSS Attacks) and server-side rendering.

Directive example

import { AfterViewInit, Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[remove-wrapper]'
})
export class RemoveWrapperDirective implements AfterViewInit {
  
  constructor(private elRef: ElementRef, private renderer: Renderer2) {}

  ngAfterViewInit(): void {

    // access the DOM. get the element to unwrap
    const el = this.elRef.nativeElement;
    const parent = this.renderer.parentNode(this.elRef.nativeElement);

    // move all children out of the element
    while (el.firstChild) { // this line doesn't work with server-rendering
      this.renderer.appendChild(parent, el.firstChild);
    }

    // remove the empty element from parent
    this.renderer.removeChild(parent, el);
  }
}

Component example

@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  styleUrls: ['./page.component.scss'],
})
export class PageComponent implements AfterViewInit {

  constructor(
    private renderer: Renderer2,
    private elRef: ElementRef) {
  }

  ngAfterViewInit(): void {

    // access the DOM. get the element to unwrap
    const el = this.elRef.nativeElement; // app-page
    const parent = this.renderer.parentNode(this.elRef.nativeElement); // parent

    // move children to parent (everything is moved including comments which angular depends on)
    while (el.firstChild){ // this line doesn't work with server-rendering
      this.renderer.appendChild(parent, el.firstChild);
    }
    
    // remove empty element from parent - true to signal that this removed element is a host element
    this.renderer.removeChild(parent, el, true);
  }
}

for some reason this solution did not work for me
C
CoTg

This works for me and it can avoid ExpressionChangedAfterItHasBeenCheckedError error.

child-component:

@Component({
    selector: 'child-component'
    templateUrl: './child.template.html'
})

export class ChildComponent implements OnInit {
@ViewChild('childTemplate', {static: true}) childTemplate: TemplateRef<any>;

constructor(
      private view: ViewContainerRef
) {}

ngOnInit(): void {
    this.view.createEmbeddedView(this.currentUserTemplate);
}

}

parent-component:

<child-component></child-component>