Angular2: replace host element with component's template
Asked Answered
B

4

34

I'm new to angular in general and to angular2 specifically. I'm trying to write a container component, which should have child components in it.

For example, container component:

@Component({
  selector: 'my-list',
  template: `
    <ul>
      <ng-content></ng-content>
    </ul>
  `
})
export class MyList {
}

Child component:

import { Component } from 'angular2/core'

@Component({
  selector: 'my-item',
  template: `
    <li>
      <ng-content></ng-content>
    </li>
  `
})
export class MyItem {
}

I'd like to make this structure:

<my-list>
    <my-item>One</my-item>
    <my-item>Two</my-item>
</my-list>

To be rendered to the following one:

<my-list>
    <ul>
        <li>One</li>
        <li>Two</li>
    </ul>
</my-list>

But instead, I have the host element of the container and the items preserved as well:

<my-list>
    <ul>
        <my-item>
            <li>One</li>
        </my-item>
        <my-item>
            <li>Two</li>
        </my-item>
    </ul>
 </my-list>

Plunk is available here

Question: is there a way to eliminate the host elements and to leave only the rendered template?

Blowgun answered 1/3, 2016 at 21:17 Comment(2)
see plnkr.co/edit/7nMTnH?p=previewGalanti
@EricMartinez, in your plunk I see that you replaced the usage of my-list and my-item by direct usage of ul and li. This is pointless. I meant my-list and my-item to be the components, which render themselves to ul and li accordingly. Of course, this is just a very simplified example. The real case is much more complex, but the main point is still the same: to eliminate (replace) host elementsBlowgun
B
14

Finally I found solution: injecting ElementRef to MyItem and using its nativeElement.innerHTML:

MyList:

import { Component, ContentChildren, QueryList } from 'angular2/core'
import { MyItem } from './myitem'

@Component({
  selector: 'my-list',
  template: `
    <ul>
      <li *ngFor="#item of items" [innerHTML]="item.innerHTML"></li>
    </ul>
  `
})
export class MyList {
  @ContentChildren(MyItem) items: QueryList<MyItem>
}

MyItem:

import { Directive, Inject, ElementRef } from 'angular2/core'

@Directive({selector: 'my-item'})
export class MyItem {
  constructor(@Inject(ElementRef) element: ElementRef) {
    this.innerHTML = element.nativeElement.innerHTML
  }
}

Working plunk is here

Blowgun answered 3/3, 2016 at 5:39 Comment(3)
@Natanael, you can find a working plunk here: plnkr.co/edit/um5BC7?p=previewBlowgun
@Natanael you have to access innerHTML inside ngAfterContentInit callback. It is empty if you access it inside constructor. I edited answer to reflect this...Prau
Fair warning: Do not use InnerHTML for anything other than static HTML. This includes bindings, other components, whatever - it will mess things upEffy
E
24

This should get you what you want:

@Component({
  selector: 'ul[my-list]',
  template: `
    <ng-content></ng-content>
  `
})
export class MyList {
}
@Component({
  selector: 'li[my-item]',
  template: `
    <ng-content></ng-content>
  `
})
export class MyItem {
...
}
<ul my-list>
    <li my-item>One</li my-item>
    <li my-item>Two</li my-item>
</ul my-list>
Extend answered 1/3, 2016 at 21:21 Comment(8)
This will work, but it's not exactly what I need. This causes the user of my component to be aware of my implementation DOM structure (ul/li) - making me much less flexible to change this structure in future (e.g. to divs)Blowgun
What you can do is to not use ul and li but my-item with CSS my-item: { display: list-item;}, then it behaves like a list item but doesn't have to be one and you can use the element selectors you had in your question (but with the template content of my answer)Meister
With CSS there might be another problem: I would like to use existing bootstrap CSS and to be able to update it smoothly. Is there a way to reuse bootstrap's styles for ul and li and to apply them on custom elements?Blowgun
Sure, but only when they are actually <ul> and <li> elements like in my answer not when used like in your question or my previous comment.Meister
@GünterZöchbauer - Is there an alternative to this solution? I tried copying in the code that you supplied and it does not work. Thanks.Shaitan
It throws this error The selector should be kebab-cased and include a dash (angular.io/guide/styleguide#style-05-02) (component-selector)tslint(1)Stamm
That's just a style rule that you can decide to follow, ignore, or comply with.Meister
Does this work in 2022 ? I tried it & got an error & the Angular interpreter did not expect the my-item of the closing tag.Cabe
B
14

Finally I found solution: injecting ElementRef to MyItem and using its nativeElement.innerHTML:

MyList:

import { Component, ContentChildren, QueryList } from 'angular2/core'
import { MyItem } from './myitem'

@Component({
  selector: 'my-list',
  template: `
    <ul>
      <li *ngFor="#item of items" [innerHTML]="item.innerHTML"></li>
    </ul>
  `
})
export class MyList {
  @ContentChildren(MyItem) items: QueryList<MyItem>
}

MyItem:

import { Directive, Inject, ElementRef } from 'angular2/core'

@Directive({selector: 'my-item'})
export class MyItem {
  constructor(@Inject(ElementRef) element: ElementRef) {
    this.innerHTML = element.nativeElement.innerHTML
  }
}

Working plunk is here

Blowgun answered 3/3, 2016 at 5:39 Comment(3)
@Natanael, you can find a working plunk here: plnkr.co/edit/um5BC7?p=previewBlowgun
@Natanael you have to access innerHTML inside ngAfterContentInit callback. It is empty if you access it inside constructor. I edited answer to reflect this...Prau
Fair warning: Do not use InnerHTML for anything other than static HTML. This includes bindings, other components, whatever - it will mess things upEffy
S
4

New angular versions have really cool directive, which may be used also for your use case. Tadaa: NgComponentOutlet. Happy coding ;)

Example:

@Component({selector: 'hello-world', template: 'Hello World!'})
class HelloWorld {
}

@Component({
  selector: 'ng-component-outlet-simple-example',
  template: `<ng-container *ngComponentOutlet="HelloWorld"></ng-container>`
})
class NgTemplateOutletSimpleExample {
  // This field is necessary to expose HelloWorld to the template.
  HelloWorld = HelloWorld;
}
Straka answered 19/12, 2017 at 11:4 Comment(4)
And also interesting variation NgTemplateOutlet.. this one did the thing for me :)Straka
Please provide a usage exampleUncommunicative
@StevenLiekens example was in docs, but I have copied it here.. ng-container element is used, which is meta element replaced during compilation.Straka
I mean can you show how to solve the asker's problem using NgComponentOutlet?Uncommunicative
P
-3

In the latest angular you can even avoid adding <ng-content></ng-content>

I've applied is like:

import {Component} from '@angular/core';

@Component({
    selector: 'div[app-root]',
    templateUrl: 'app.component.html',
    styleUrls: ['app.component.css']
})
export class AppComponent {
    title = 'Delegates Presence Reporting';
}

and the template:

<div class="centered">
<h1>{{title}}</h1>
</div>

index.html

<body>
<div app-root></div>
</body>
Postaxial answered 7/3, 2018 at 10:44 Comment(1)
Nothing gets replaced. The output will be a div class="centered" inside the div app-root.Kofu

© 2022 - 2024 — McMap. All rights reserved.