192

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 using a child component to encapsulate the rendering of the table row without the wrapping <my-result> tag?

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.

Ole
  • 41,793
  • 59
  • 191
  • 359
Greg
  • 3,370
  • 3
  • 18
  • 20
  • can you please show a working example ? – zakaria amine Dec 06 '18 at 09:08
  • 4
    The right answer is there, with a plunker sample https://stackoverflow.com/questions/46671235/remove-host-component-tag-from-html-in-angular-4 – sancelot Jan 30 '19 at 15:11
  • 3
    None of the proposed answer are working, or are complete. The right answer is described here with a plunker sample https://stackoverflow.com/questions/46671235/remove-host-component-tag-from-html-in-angular-4 – sancelot Jan 30 '19 at 15:36

8 Answers8

164

You can use attribute selectors

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

and then use it like

<td myTd></td>
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Does the selector of your component contain the `[]`? Did you add the component to `declarations: [...]` of a module? If the component is registered in a different module, did you add this other module to `imports: [...]` of the module where you want to use the component? – Günter Zöchbauer Aug 25 '17 at 10:59
  • Actually I am getting a different error, sorry for deleting my previous comment it was irrelevant. My error is this: `ERROR TypeError: el.setAttribute is not a function` – Aviad P. Aug 25 '17 at 11:00
  • My usage is as follows `` – Aviad P. Aug 25 '17 at 11:00
  • I assume `el.setAttribute` is custom code of yours. Perhaps a `this.` is missing, or something else is wrong in this code. – Günter Zöchbauer Aug 25 '17 at 11:03
  • 2
    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 `` – Aviad P. Aug 25 '17 at 11:05
  • 2
    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. – Robouste Nov 06 '17 at 13:30
  • 2
    @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: `` I used this (after changing li-navigation to an attribute selector) `` – Mahesh Nov 11 '17 at 21:20
  • @BharatDBhadresha I don't understand what your edit suggestion is for. – Günter Zöchbauer Feb 15 '19 at 13:58
  • I tried to use this method with but it didn't work -Error on setAttribute for ng-container element-, i want the component to decide which item to print not use td as mentioned, anyone can help? – Anas Mar 10 '19 at 11:33
  • 6
    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 – Rusty Rob May 23 '19 at 00:03
  • In case you need to pass an object to embedded component. – AjitChahal Nov 10 '19 at 20:07
  • This doesn't work. dunno why it's everywhere as a solution. "The selector should be used as an element (https://angular.io/guide/styleguide#style-05-03) (component-selector)tslint(1)" – Don Dilanga Jan 08 '20 at 17:41
  • 2
    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`](https://angular.io/api/core/ViewContainerRef) (check [@Slim's answer below](https://stackoverflow.com/a/56887630/979505)). 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`. – Daniel Mar 12 '20 at 22:14
  • That's great but I want *no* tag, instead of a div/td/whatever with myTd attribute – gyozo kudor Apr 16 '20 at 12:45
  • @gyozokudor I believe you, but I didn't develop Angular. So all I could do is to provide an answer that comes as close as possible while being limited by what Angular provides and by my limited knowledge of the world. – Günter Zöchbauer Apr 16 '20 at 14:31
  • Also, the angular community kinda didn't like this approach https://angular.io/guide/styleguide#style-05-03 – karina Sep 28 '20 at 11:56
  • @karina I agree that you shouldn't do this without a specific requirement like the one in the original question. – Günter Zöchbauer Sep 28 '20 at 13:17
  • I don't know why this has so many upvotes? `` injects the component, which in turn makes it ` Lisa 333 `. Which doesn't get rid of the nesting. – alex351 Apr 05 '23 at 10:31
  • @alex351becaus it's probably still the best you can get. So when no perfect solution exists, should then no answer being posted instead of what's the best we got so far? – Günter Zöchbauer Apr 05 '23 at 11:42
  • 1
    Just to be clear, your answers helped me a lot of times and I appreciate your effort more than most of the others here. But https://stackoverflow.com/a/56887630/3934886 answer comes much closer to the request. table > th > td can't handle angular tags inside well. – alex351 Apr 11 '23 at 22:55
  • @alex351 thank you for the kind words. No worries, everything that helps visitors solve their problems is highly welcome! My answer reflects my knowledge at the time. Sadly, I don't have the time and energy anymore to keep my answers updated. – Günter Zöchbauer Apr 12 '23 at 08:03
71

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', { static: true }) template;


constructor(
  private viewContainerRef: ViewContainerRef
) { }

ngOnInit() {
  this.viewContainerRef.createEmbeddedView(this.template);
}
Steffen
  • 3,999
  • 1
  • 23
  • 30
G. Modebadze
  • 785
  • 6
  • 7
  • 10
    Works like a charm! Thank you. I used in Angular 8 the following instead: `@ViewChild('template', {static: true}) template;` – AlainD. Aug 07 '19 at 14:07
  • 5
    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. – RandallTo Feb 14 '20 at 20:20
  • 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`. – Daniel Mar 12 '20 at 22:11
  • 1
    When I use this how can I avoid getting ExpressionChangedAfterItHasBeenCheckedError? – gyozo kudor Apr 16 '20 at 14:09
  • @gyozokudor maybe using a setTimeout – Diego Fernando Murillo Valenci Aug 16 '20 at 23:21
  • Could anybody please provide an example of this? – Pragmatick Mar 21 '21 at 10:16
  • This seems to create the element next to an empty element with the selector's name. – Rei Miyasaka Apr 02 '21 at 02:36
  • If you need to delete component tags completely, you can do it manually in the parent **ngAfterViewInit**: `document.querySelectorAll('').forEach(el => el.parentNode.removeChild(el));` – Eugene P. Jun 10 '21 at 10:19
  • 3
    If anyone has errors with this.viewContainerRef, move code from ngOnInit to ngAfterViewInit. – Cafn Aug 18 '21 at 12:04
  • 3
    Thanks! This is the only approach that worked for me. In addition to your code, I also do `this.viewContainerRef.element.nativeElement.remove()` at the end, to remove the original (empty) component. – ZolaKt May 19 '22 at 12:43
  • Thanks, it also works for me. I needed this because NgBootstrap created automatically ` – ToddEmon Nov 11 '22 at 14:20
  • Is it possible to use `createEmbeddedView` with projected content instead of a `ng-template`? The idea was to be able to re-create/re-render the originally projected content when needed. – TCB13 May 06 '23 at 10:00
  • I tried this and it still renders the containing element. [Here's a Stackblitz](https://stackblitz.com/edit/stackblitz-starters-jvbrq8?file=src%2Fmain.ts). How do we get it to work without rendering the containing element? – Ole Aug 20 '23 at 18:38
47

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>
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116
  • 1
    *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) – M_Idrees Feb 06 '22 at 09:05
  • doesn't answer the original question: "render a component without its wrapping tag" – alex351 Apr 05 '23 at 10:38
  • 1
    I think it's worth noting, that using `display: contents` may cause some accessibility issues, so it should rather be used with caution (at least until it's totally safe in all major browsers): https://caniuse.com/css-display-contents – ScriptyChris Apr 30 '23 at 17:42
23

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

Nicholas Murray
  • 13,305
  • 14
  • 65
  • 84
  • 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. – Don Dilanga Jan 08 '20 at 17:46
14

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>
Shlomi Aharoni
  • 482
  • 7
  • 9
  • 8
    This is throwing an error because 'parentElement.parentNode' is null – emirhosseini Feb 21 '19 at 02:10
  • 3
    Won't this mess with Angular's change detection and virtual DOM? – Ben Winding Mar 16 '21 at 03:42
  • @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. – Ali Almutawakel May 31 '21 at 21:06
  • this solution doesn't work, throws null and destroys the ng structure – Shaybc Nov 14 '21 at 12:53
  • This removes the component altogether – Bopsi Dec 06 '21 at 07:30
8

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.

mjswensen
  • 3,024
  • 4
  • 28
  • 26
3

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);
  }
}
Ali Almutawakel
  • 309
  • 2
  • 7
2

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>
CoTg
  • 91
  • 1
  • 6