Correct way to implement wrapper components in Angular 4?
Asked Answered
P

1

9

Update:

The correct question is probably: "how do I implement my own version of ng-container so I can use "<my-component ...>...</my-component>" instead of "<ng-container my-directive ...>...</ng-container>".

I am developing a new app in Angular 4. I want to build my entire app with wrapper components so I can swap out the actual components if need be. This works easily for simple controls but not for complex controls we want to break up into separate components. The best example is with tabs because the requirements are fairly stable: a list of tabs with labels and content panels that we show/hide.

ng-bootstrap may use something like this:

<ngb-tabset>
  <ngb-tab title="Tab 1">
    <ng-template ngbTabContent>...</ng-template>
  </ngb-tab>
  ...
 <ngb-tabset>

We may have some other component like this:

<div class="my-tab-group">
  <ul>
    <li>Tab 1</li>
    ...
  </ul>
  <div class="my-tab" title="Tab 1">...</div>
  ...
</div>

Instead of tying my app to a specific implementation at this time I want to define my own tabs wrapper and use it everywhere like this:

<my-tabs-control>
  <my-tab-control title="Tab 1">...</my-tab-control>
  ...
<my-tabs-control>

Then if I need to change the way my tabs work it happens in one place. However, if we use a wrapper component then the HTML gets polluted with the host element tags interlaced with the desired tags which obviously messes things up, e.g.

<my-tabs-control>
  <div class="my-tab-group">
    <ul>
      <li>Tab 1</li>
      ...
    </ul>
    <my-tab-control title="Tab 1">
      <div class="my-tab" title="Tab 1">...</div>
    </my-tab-control>        
    ...
  </div>
<my-tabs-control>

My guess is this is the reason why a lot of people ask how to unwrap/remove the host element in an Angular component - that would make this really simple to do.

The usual answer is to use attribute selectors instead, e.g.:

Remove the angular2 component's selector tag

How to remove/replace the angular2 component's selector tag from HTML

However, this means we need to know the tag structure at the usage site, which obviously destroys the whole point of using nicely named wrapper tags.

Thus, finally, how should I be doing this? Is it perhaps not possible due to performance reasons? I have started looking at Renderer2 but have not found an obvious way to do what I want, and I want to avoid non-angular DOM hacks.

Patroon answered 8/9, 2017 at 13:22 Comment(2)
Looks like you want to use 'transclusion' using ng-content: scotch.io/tutorials/angular-2-transclusion-using-ng-content Official angular docs seem to be missing this: github.com/angular/angular.io/issues/3099Disentangle
i use ng-content a lot already for projected content, but this does not address the problem of the wrapper/container tags created by the component itself. We can sort of get what we want using ng-container with a directive instead of a component, but that looks messy. We can maybe use a directive with an element selector instead of the default attribute selector and then (maybe?) use Renderer2/code to render the content. What I really want is a switch to simply say "render the contents of this component without the wrapper/container/host element. Sadly, I think that is not possible.Patroon
P
4

I have a solution that works, although not as elegant as I would have liked. I would much prefer an option to just "get rid of the host elements" as that would be much neater.

I derived this solution from simply checking out how they implemented tabs in the ng-bootstrap project at : https://github.com/ng-bootstrap/ng-bootstrap.

The solution is to use an @Component as the main container and then use @Directives for all the internal child components. Normally we use a directive as an add-on to some other element - but here we use the directive as the actual element. This seems to work like a component that does not render it's own content nor create a host container of its own. Instead it allows the host component to decide what to do with the directive's content. The extra condition is we have to use ng-templates - I cannot see how to project the directive content directly without using ng-templates.

Here is how I markup a tab container using my wrapper component:

<my-tab-container>
  <my-tab title="Tab 1">
    <ng-template myTabContent>
      This is panel 1!
    </ng-template>
  </my-tab>
  <my-tab title="Tab 2">
    <ng-template myTabContent>
      This is panel 2!
    </ng-template>
  </my-tab>
  <my-tab title="Tab 3">
    <ng-template myTabContent>
      This is panel 3!
    </ng-template>
  </my-tab>
</my-tab-container>

Here is a minimal implementation of my tabs wrapper component, which includes the two directives for individual tabs and tab content.

import { Component, OnInit, Directive, ContentChildren, QueryList, Input, TemplateRef, ContentChild } from '@angular/core';

@Directive({ selector: 'ng-template[myTabContent]' })
export class TabContentDirective {
  constructor(public templateRef: TemplateRef<any>) { }
}

@Directive({ selector: 'my-tab' })
export class TabDirective {
  @Input()
  title: string;
  @ContentChild(TabContentDirective)
  content: TabContentDirective;
  public getTemplateRef() {
    return this.content.templateRef;
  }
}

@Component({
  selector: 'my-tab-container',
  templateUrl: './tab-container.component.html',
  styleUrls: ['./tab-container.component.scss']
})
export class TabContainerComponent implements OnInit {
  @ContentChildren(TabDirective)
  tabs: QueryList<TabDirective>;
  constructor() { }
  ngOnInit() {
  }
}

Now I can render different types of tabs by switching out the HTML template for my tab container component.

e.g. for ng-bootstrap:

<ngb-tabset>
  <ngb-tab *ngFor="let tab of tabs">
    <ng-template ngbTabTitle>{{tab.title}}</ng-template>
    <ng-template ngbTabContent>
      <ng-template [ngTemplateOutlet]="tab.content.templateRef"></ng-template>
    </ng-template>
  </ngb-tab>
</ngb-tabset>

e.g. for Angular Material2:

<md-tab-group>
  <md-tab *ngFor="let tab of tabs" label="{{tab.title}}">
    <ng-template [ngTemplateOutlet]="tab.content.templateRef"></ng-template> 
  </md-tab>
</md-tab-group>

or some other custom thing (jQueryUI style):

<ul>
  <li *ngFor="let tab of tabs">{{tab.title}}</li>
</ul>
<div *ngFor="let tab of tabs">
  <ng-template [ngTemplateOutlet]="tab.content.templateRef"></ng-template>
</div>

It means we can now carry on adding tabs (and all other types of controls) to the UI all over the place without worrying if we've selected the best components for our project - we can easily switch things around later if required.

(hopefully this does not introduce performance problems!)

Patroon answered 14/9, 2017 at 15:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.