Structural Directive in Angular
Overview
AngularJS comes with a unique feature of directives. Directives do help to extend the HTML vocabulary. Similar thing is carried forward to Angular as well. The only difference between AngularJS vs Angular directive is, Angular directive does not contain a template. Essentially directives attach behavior on the DOM node. Structural Directive is a special kind of directive, it helps to add/remove/replace/ repeat templates on DOM based on the expression supplied to it.
Introduction to Structural Directives
Structural directives help to add or remove templates on DOM conditionally. Angular Framework provides three built-in structural directives like ngFor, ngIf, and ngSwitch. When we use structural directives prefixed with * like *ngFor, *ngIf, or *ngSwitch, angular internally transforms this * into ng-template. And this template is used by the respective directive to add or remove a template inside a DOM tree.
When do We Use Structural Directives?
The structural directive can be used in a situation
- Add / Remove DOM template based on expression
- Repeat DOM template based on the expression
Structural Directive Shorthand
Shorthand | transformed to |
---|---|
*ngFor="let item of [1,2,3]" | <ng-template ngFor let-item [ngForOf]="[1,2,3]"> |
*ngFor="let item of [1,2,3] as items; trackBy: myTrack; index as i" | <ng-template ngFor let-item [ngForOf]="[1,2,3]" let-items="ngForOf" [ngForTrackBy]="myTrack" let-i="index"> |
*ngIf="exp" | <ng-template [ngIf]="exp"> |
*ngIf="exp as value" | <ng-template [ngIf]="exp" let-value="ngIf"> |
Commonly Used Structural Directives
There are 3 built-in structural directives provided by Angular provides
NgIf
ngIf is the most frequently used angular structural directive. It helps to conditionally add or remove templates.
Syntax
Usage
Above html conditionally displays Logged in as {{ user.name }} when isLoggedIn flag, and not displayed when isLoggedIn is false.
NgFor
ngFor is used for repeat template multiple times as like a for loop we use in ES6. The syntax is quite similar to for of, like for (let item of items)
Syntax
Usage
Given the template on which ngFor directive is applied is repeated the names.length times.
Syntax | description |
---|---|
let item of items | Loop over items |
index | Value from context, index of current iteration |
trackBy | Callback returns a value, that should be compared , if mismatch render specific view again. |
Output
NgSwitch
ngSwitch directive is the same as switch statement of Angular. Multiple templates can be specified inside a condition with a combination of *ngSwitchWhen (for cases) and *ngDefaultSwitch (for default templates).
Syntax
Usage
Above example demonstrate the usage of ngSwitch directive, like JS switch case switch(type) { case 1: break; default: break; }.
One Structural Directive Per Element
While using a structural directives,
You can not use two structural directives on a single element, that's a limitation. Technically if you think of doing it, it won't possible to achieve.
If you want to apply two structural directives like
You have to move a structural directive on a different level. Perhaps using the ng-container element instead of an extra div makes sense here, as it won't create any element.
Creating Custom Structural Directives
In a big enterprise-grade application, feature flag-driven development is followed. Feature flag development enables us to control features on the distribution level. We can easily enable or disable features by toggling flags on different environments. We've to build a custom structural directive that would display the content only when the specified feature flag is enabled.
Usage
We have defined a FeatureEnabledDirective class
- with selector [isFeatureEnabled]
- constructor has TemplateRef and ElementRef
- console log templateRef and el
When directive code executes it prints below-console log.
One thing to note here is el is converted to comment. Ideally we would expect to see <div>Test is enabled</div> element. But no it consoled comment. This is the magic of * in front of the directive name. Basically, when we add * (star) in front of the directive, it started working as a structural directive. And complete HTML is wrapped inside ng-template.
The above template would be converted as
Hence if we do TemplateRef inside the constructor, we would receive the ng-template which we can use later to add or remove the directive element.
Thus, we need ViewContainerRef to get hold of the directive element. And then we would define the Input property on the same name as the directive selector isFeatureEnabled.
The isFeatureEnabled setter accepts the input value and clears the viewContainerRef every time at the beginning. Afterward, add the content only if the feature exists in enabledFeatures variable.
Testing the Directive
To locally test the directive behavior we can try testing both +ve and -ve scenarios.
-
Pass valid value, it should display on view.
-
Pass invalid value, it should not display on view.
Structural Directive Syntax Reference
Structural directive syntax can be written in multiple formats, below is syntax expression, and how it looks in general.
Syntax
Shorthand | transformed to |
---|---|
prefix | HTML attribute |
key | HTML attribute |
local | Local variable name used on a template |
export | The name with directive instance should be exported |
expression | an ordinary expression |
Example
How Angular Translates Shorthand
Translating shorthand work has been done by Angular Compiler. Though below are the template translation you can see.
Shorthand | translates to |
---|---|
prefix and naked expression | [prefix]="expression" |
keyExp | [prefixKey] "expression" (let-prefixKey="export") |
let | let-local="export" |
Improving Template Type Checking for Custom Directives
It will be an insult to typescript if we declare or use any type. We can make our directive use a type-checking mechanism in an efficient manner. To enforce this typing check, we have to do following things
- For narrow downing type we can use ngTemplateGuard_(someInputProperty) static property
- static property ngTemplateContextGuard helps to guard directive template context
Making in-template Type Requirements More Specific with Template Guards
We can narrow down the input property type if union type is used. One out of the union type should be strictly set on the template. Right now for the isFeatureEnabled directive, the Input property is of a type string. If we would use union type, we could strictly narrow down the type on the template
So on template, it would expect the object to be of type B. A, C and D Skelton won't work on UI. It will throw an error.
Typing the Directive’s Context
To make directive context available outside and type-safe, we can use ngTemplateContextGuard static function to achieve it. But before that, we have to create a class that holds the schema of context
The context contains a roles string collection, and isSuperUser getter that checks whether input role is super_user or not.
So far we have our TemplateRef of type any, we would like to make it type safe, right? Let's create a directive context and typesafe the directive. Next task is to set FeatureEnabledDirective context using ngTemplateContextGuard static method.
Let's look at the details of ngTemplateContextGuard method.
- directive (FeatureEnabledDirective) as a first parameter
- context (unknown) as a second parameter
- Return type is set to FeatureEnabledContext (the directive context) using is operator.
- return true from the function, that makes sure the directive is typesafe.
From lines 16-23, filter logic is applied to find a matching feature from a collection. If feature is found, roles are filled inside context.
Let's understand what changes are applied.
- 'super_user' is a value passed to isFeatureEnabled
- roles are taken out of context, and filled in inside let roles
- roles have been used on HTML.
If we try *isFeatureEnabled="'super_user'; let roles = role", a typo for role, TS will complain about the property name.
Conclusion
- How to use built-in directives.
- Long hand and syntax shorthand.
- How one can create narrow down types of directive
- How to expose typed directive context