Angular2 : render a component without its wrapping tag

JavascriptAngular

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

You can use attribute selectors

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

and then use it like

<td myTd></td>

Solution 2 - Javascript

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

Solution 3 - Javascript

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>

Solution 4 - Javascript

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

Solution 5 - Javascript

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>

Solution 6 - Javascript

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.

Solution 7 - Javascript

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

Solution 8 - Javascript

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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGregView Question on Stackoverflow
Solution 1 - JavascriptGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - JavascriptG. ModebadzeView Answer on Stackoverflow
Solution 3 - Javascriptrobert kingView Answer on Stackoverflow
Solution 4 - JavascriptNicholas MurrayView Answer on Stackoverflow
Solution 5 - JavascriptShlomi AharoniView Answer on Stackoverflow
Solution 6 - JavascriptmjswensenView Answer on Stackoverflow
Solution 7 - JavascriptAli AlmutawakelView Answer on Stackoverflow
Solution 8 - JavascriptCoTgView Answer on Stackoverflow