Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to implement wrapper components in Angular 4?

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.

like image 451
Etherman Avatar asked Sep 08 '17 13:09

Etherman


People also ask

What is wrapper component in angular?

angular-wrapper is an NPM module which is exposing Angular. js as a CommonJS module. This library is particularly useful if your project is using Webpack module bundler. Angular.

How do you use Ngcontent?

The ng-content is used when we want to insert the content dynamically inside the component that helps to increase component reusability. Using ng-content we can pass content inside the component selector and when angular parses that content that appears at the place of ng-content.

How we can add style to the particular component in angular?

There are several ways to add styles to a component: By setting styles or styleUrls metadata. Inline in the template HTML. With CSS imports.

What is Contentprojection?

Content projection is a pattern in which you insert, or project, the content you want to use inside another component. For example, you could have a Card component that accepts content provided by another component.


1 Answers

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

like image 143
Etherman Avatar answered Sep 29 '22 21:09

Etherman