Leverage Structural Directives to Create Powerful Components in Angular

June 13, 2017 0 Comments

Leverage Structural Directives to Create Powerful Components in Angular

 

 

In this article, we will learn how to create a powerful dropdown component with the help of structural directives.

Before you start I highly recommend you to read my article — The Power of Structural Directives in Angular.

We are not going to waste time on styling. We will take the CSS bootstrap dropdown style.

This will be our final result.

Let’s start with creating the component. I will omit the CSS classes so that we can focus on the important things.

@Component({
selector: 'nb-dropdown',
template: <br> <code class="markup--code markup--pre-code">&lt;div class=&quot;dropdown&quot; </code>[class.open]=&quot;isOpen&quot; (click)=&quot;isOpen = !isOpen&quot;<code class="markup--code markup--pre-code u-paddingRight0 u-marginRight0">&gt;<br> &lt;button&gt;<br> <strong class="markup--strong markup--pre-strong">&lt;!-- We need to put here the selected option --&gt;</strong><br> &lt;span class=&quot;caret&quot;&gt;&lt;/span&gt;<br> &lt;/button&gt;<br> &lt;ul&gt;</code> <br><code class="markup--code markup--pre-code u-paddingRight0 u-marginRight0"> <strong class="markup--strong markup--pre-strong">&lt;!-- We need to loop over the options --&gt;</strong><br> &lt;li&gt;<br> &lt;a href=&quot;#&quot;&gt;Option&lt;/a&gt;<br> &lt;/li&gt;<br> &lt;/ul&gt;<br> &lt;/div&gt;</code><br>,
})
export class NbDropdownComponent {}

We need a way to inject custom templates from outside, for the selected option and each option.

Let’s create two structural directives that will help us with this.

For the selected option —

import { Directive, TemplateRef } from '@angular/core';

@Directive({
selector: '[nbSelectMatch]'
})
export class NbSelectMatchDirective {

constructor( private tpl : TemplateRef<any> ) {
}

}

For each option —

import { Directive, Input, TemplateRef } from '@angular/core';

@Directive({
selector: '[nbSelectOption]'
})
export class NbSelectOptionDirective {
option;

constructor( private tpl : TemplateRef<any> ) {
}

@Input() set nbSelectOption( option ) {
this.option = option;
}

}

Nothing fancy here, we are just injecting the TemplateRef in both directives so that we can use them later in the parent component. Notice that in the NbSelectOptionDirective we also take as Input the option.

Now we can use them in our component.

<nb-dropdown>
  <ng-template nbSelectMatch let-selected>
<i class="fa fa-{{selected?.icon}}"></i>
{{selected?.title}}
</ng-template>

<ng-container ngFor="let browser of browsers">
<ng-template [nbSelectOption]="browser">
<i class="fa fa-{{browser.icon}}"></i>
{{browser.title}}
</ng-template>
</ng-container>

</nb-dropdown>

We do not need additional HTML tags, and that is why we are using ng-container. We will return soon to the part of the let-selected.

Now let’s get a reference to these directives in the nb-dropdown component and display the templates in the appropriate places.

export class NbDropdownComponent {
@ContentChild(NbSelectMatchDirective) match;
@ContentChildren(NbSelectOptionDirective) nbSelectOptions;
}

We can use the @ContentChild and @ContentChildren decorators to get a reference to the directives instances. Now we can use them in our template.

@Component({
selector: 'nb-dropdown',
template: <br> <code class="markup--code markup--pre-code">&lt;div class=&quot;dropdown&quot; </code>[class.open]=&quot;isOpen&quot; (click)=&quot;isOpen = !isOpen&quot;<code class="markup--code markup--pre-code">&gt;<br> &lt;button&gt;<br> </code><strong class="markup--strong markup--pre-strong">&lt;ng-container [ngTemplateOutlet]=&quot;match.tpl&quot; [ngOutletContext]=&quot;selected&quot;&gt;</strong><br> &lt;/ng-container&gt;<br> &lt;span *ngIf=&quot;!selected.$implicit&quot;&gt;{{placeholder}}&lt;/span&gt; <code class="markup--code markup--pre-code"><br> &lt;/button&gt;<br> &lt;ul&gt;</code> <code class="markup--code markup--pre-code"><br> </code> &lt;li *ngFor=&quot;let option of <strong class="markup--strong markup--pre-strong">nbSelectOptions</strong>&quot;&gt;<br> &lt;a href=&quot;#&quot;&gt;<br> <strong class="markup--strong markup--pre-strong">&lt;ng-container [ngTemplateOutlet]=&quot;option.tpl&quot;&gt;<br> &lt;/ng-container&gt;</strong><br> &lt;/a&gt;<br> &lt;/li&gt;<code class="markup--code markup--pre-code"><br> &lt;/ul&gt;<br> &lt;/div&gt;</code><br>,
})
export class NbDropdownComponent {
@Input() placeholder;
@ContentChild(NbSelectMatchDirective) match;
@ContentChildren(NbSelectOptionDirective) nbSelectOptions;
  selected = {}
}

We can use ngTemplateOutlet to embed a view from a prepared TemplateRef and ngOutletContext to pass a context to the embedded view ( this is for the let-selected part ). We also add an Input for a placeholder.

The last thing is to create a custom form control for our component.

export const VALUE_ACCESSOR : any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NbDropdownComponent),
multi: true,
};


@Component({
selector: 'nb-dropdown',
template: <br> <code class="markup--code markup--pre-code">&lt;div class=&quot;dropdown&quot; </code>[class.open]=&quot;isOpen&quot; (click)=&quot;isOpen = !isOpen&quot;<code class="markup--code markup--pre-code">&gt;<br> &lt;button&gt;<br> </code>&lt;ng-container [ngTemplateOutlet]=&quot;match.tpl&quot; [ngOutletContext]=&quot;selected&quot;<strong class="markup--strong markup--pre-strong">&gt;</strong><br> &lt;/ng-container&gt;<br> &lt;span *ngIf=&quot;!selected.$implicit&quot;&gt;{{placeholder}}&lt;/span&gt; <code class="markup--code markup--pre-code"><br> &lt;/button&gt;<br> &lt;ul&gt;</code> <code class="markup--code markup--pre-code"><br> </code> &lt;li *ngFor=&quot;let option of nbSelectOptions&quot; <strong class="markup--strong markup--pre-strong">(click)=&quot;select(<em class="markup--em markup--pre-em">option</em>)&quot;</strong>&gt;<br> &lt;a href=&quot;#&quot;&gt;<br> &lt;ng-container [ngTemplateOutlet]=&quot;option.tpl&quot;&gt;<br> &lt;/ng-container&gt;<br> &lt;/a&gt;<br> &lt;/li&gt;<code class="markup--code markup--pre-code"><br> &lt;/ul&gt;<br> &lt;/div&gt;</code><br>,
providers: [VALUE_ACCESSOR]
})
export class NbDropdownComponent implements ControlValueAccessor {
isOpen = false;
@Input() placeholder = '...';
@ContentChild(NbSelectMatchDirective) match;
@ContentChildren(NbSelectOptionDirective) nbSelectOptions;
selected = {}

writeValue( selected : any ) : void {
if( selected ) {
this.setSelected(selected);
}
}

registerOnChange( fn : any ) : void {
this.onChange = fn;
}
  registerOnTouched( fn : any ) : void {
// add on touched...
}
  select( nbSelectMatch ) {
this.setSelected(nbSelectMatch);
this.onChange(nbSelectMatch.option);
}

setSelected( nbSelectMatch ) {
this.selected = {
$implicit: nbSelectMatch.option ?
nbSelectMatch.option :
nbSelectMatch
}
}

}

As you may know, if you want to create custom form control in Angular, you need to implement the ControlValueAccessor interface. I’m not going to go into this topic because it requires an article of its own. You can read more about the subject here.

Now let’s see the final component but this time we the syntactic sugar syntax.

<nb-dropdown [formControl]="browser" placeholder="Choose browser">

<ng-container
nbSelectMatch="let selected">
<i class="fa fa-{{selected?.icon}}"></i>
{{selected?.title}}
</ng-container>

<ng-container ngFor="let browser of browsers">
<ng-container
nbSelectOption="browser">
<i class="fa fa-{{browser.icon}}"></i>
{{browser.title}}
</ng-container>
</ng-container>

</nb-dropdown>

We saw how we could create a powerful component with the help of structural directives. We have implemented only the basic of our dropdown, but you can take it from here to do anything that you need.

If you lack understanding of any part of the article you can find the answer in the article I mentioned at the beginning.

You can play with the code here.

Follow me on Medium or Twitter to read more about Angular, Vue and JS!


Tag cloud