Creating Directives page
Learn how to create Directives in Angular that can change the appearance or behavior of DOM Elements.
Problem
While filling out the phone number
field, you might have noticed that users can type in both letters and numbers. This is a problem because we do not want users entering letters in the phone number
field.
In order to fix this, we will create an Attribute Directive that will change the behavior of the Phone Input Field, and will ensure that only numbers can be entered in the field.
<input
name="phone"
type="text"
pmoOnlyNumbers
formControlName="phone"
/>
What you need to know
- What are Directives?
- What is
ElementRef
? - What are
HostListeners
? - How to create a Directive
What are Directives?
Directives are classes that tell Angular to change the appearance or behavior of DOM Elements. Angular comes with a set of Built-in Directives, and they consist of three types:
- Components
- Structural Directives
- Attribute Directives
Components
In this training, we previously talked about Components.
Components are a type of Directive. Components use the @Component
decorator function along with a template, style, and other logic needed for the view.
This was previously discussed in detail here. The official Angular documentation has more information on this as well.
import { Component } from '@angular/core';
@Component({
selector: 'app-button',
template: `<button>Click me!</button>`
})
export class AppButtonComponent {}
Structural Directives
Structural Directives are types of Directives that are used to change HTML DOM layout by adding, removing or manipulating Elements. Learn more about structural directives.
import { Component } from '@angular/core';
@Component({
selector: 'app-message',
template: `
<p *ngIf="showMessage">
Conditional message!
</p>
`
})
export class AppMessageComponent {
showMessage: boolean = true;
// You can toggle this value to show or hide the message
toggleMessage(): void {
this.showMessage = !this.showMessage;
}
}
Attribute Directives
Attribute Directives are a type of directive that are mainly used to listen or change the behavior or appearance of DOM Elements, Attributes and Components. Learn more about attribute directives.
Here’s a small example of building a directive (we will dig into this more in a section below):
import { Directive, ElementRef, Input } from '@angular/core';
@Directive({
selector: '[appColor]'
})
export class AppColorDirective {
@Input('appColor') set color(color: string) {
this.el.nativeElement.style.color = color;
}
constructor(private el: ElementRef) {}
}
ElementRef
ElementRef (Element Reference) is a wrapper around a native DOM Element inside of a View.
The ElementRef class contains a property nativeElement
, which references the underlying DOM object that we can use to manipulate the DOM.
Read more.
When creating a Directive, we use ElementRef to gain reference to the native HTML Element, which the Directive will be used on, and perform any manipulation we need to.
import { Directive, ElementRef, Renderer2, OnInit } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
constructor(private el: ElementRef, private renderer: Renderer2) { }
ngOnInit() {
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
}
}
Here’s how we would use this directive in a component:
import { Component } from '@angular/core';
selector: 'app-button',
template: `
<p appHighlight>
This paragraph will be highlighted in yellow.
</p>
`
})
export class AppButtonComponent {}
HostListener
HostListener is a function decorator that allows you to listen and handle DOM events from the host element.
Some examples of events include keyboard
and mouse
events.
Read more.
When creating a Directive, we use the @HostListener decorator in the Directive Class to listen for host events. Based on the event, we can perform any action needed.
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[appClickTracker]'
})
export class ClickTrackerDirective {
@HostListener('click', ['$event'])
onClick(event: MouseEvent) {
console.info('Element clicked:', event);
}
@HostListener('mouseenter')
onMouseEnter() {
console.info('Mouse entered');
}
@HostListener('mouseleave')
onMouseLeave() {
console.info('Mouse left');
}
}
Here’s how we would use this directive in a component:
import { Component } from '@angular/core';
selector: 'app-button',
template: `
<div appClickTracker>
Click or hover over this div.
</div>
`
})
export class AppButtonComponent {}
How to Generate a Directive via the CLI
ng generate directive onlyNumbers
This will generate the directive file: only-numbers.directive.ts
and the spec file: only-numbers.directive.spec.ts
How to Build a Directive
As we have discussed above, Directives are very useful tools in Angular that can help improve your web application. The example below shows an Attribute Directive that would allow a user to enter only letters
in an input field.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION, Directive, ElementRef, HostListener } = ng.core;
@Directive({
selector: '[pmoOnlyLetters]',
})
class OnlyLettersDirective {
allowedKeys: string[] = ['Backspace', 'ArrowLeft', 'ArrowRight'];
regExp: RegExp = new RegExp(/^[A-Za-z]*$/g);
constructor(private elementRef: ElementRef) {}
@HostListener('keydown', ['$event'])
onKeyDown(keyboardEvent: KeyboardEvent) {
if (this.allowedKeys.indexOf(keyboardEvent.key) !== -1) {
return;
}
const inputNativeElementValue = this.elementRef.nativeElement.value;
const next = `${inputNativeElementValue}${keyboardEvent.key}`;
if (next && !next.match(this.regExp)) {
keyboardEvent.preventDefault();
}
}
}
@Component({
selector: 'my-app',
template: `
<h2>Allow Only Letters Directive</h2>
<input name="phone" type="text" pmoOnlyLetters>
`
})
class AppComponent {
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [ BrowserModule, CommonModule],
declarations: [AppComponent, OnlyLettersDirective],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Technical requirements
- Use an
onlyNumber
Directive in src/app/order/order.component.html in the phone number input field. Using the Directive should look like this:
<input
name="phone"
type="text"
pmoOnlyNumbers
formControlName="phone"
/>
- Generate and implement the
onlyNumber
Directive.
The Directive will be used to listen for the event on the input field. This Directive will be used in our order form phone number field.
Hint: Use regex
regExp: RegExp = new RegExp(/^[0-9]*$/g)
to test if the input value contains any letters.
Setup
✏️ Update src/app/order/order.component.html file to use the Directive we will create:
<div class="order-form">
<ng-container *ngIf="orderComplete; else showOrderForm"></ng-container>
<ng-template #showOrderForm>
<h2>Order here</h2>
<form
*ngIf="orderForm && restaurant"
[formGroup]="orderForm"
(ngSubmit)="onSubmit()"
>
<tabset>
<tab heading="Lunch Menu" *ngIf="restaurant.menu.lunch">
<ul class="list-group">
<pmo-menu-items
[items]="restaurant.menu.lunch"
formControlName="items"
></pmo-menu-items>
</ul>
</tab>
<tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
<ul class="list-group">
<pmo-menu-items
[items]="restaurant.menu.dinner"
formControlName="items"
></pmo-menu-items>
</ul>
</tab>
</tabset>
<div class="form-group">
<label class="control-label">Name:</label>
<input name="name" type="text" formControlName="name" />
<p>Please enter your name.</p>
</div>
<div class="form-group">
<label class="control-label">Address:</label>
<input name="address" type="text" formControlName="address" />
<p class="help-text">Please enter your address.</p>
</div>
<div class="form-group">
<label class="control-label">Phone:</label>
<input
name="phone"
type="text"
pmoOnlyNumbers
formControlName="phone"
/>
<p class="help-text">Please enter your phone number.</p>
</div>
<div class="submit">
<h4>Total: ${{ orderTotal }}</h4>
<div class="loading" *ngIf="orderProcessing"></div>
<button
type="submit"
[disabled]="!orderForm.valid || orderProcessing"
class="btn"
>
Place My Order!
</button>
</div>
</form>
</ng-template>
</div>
✏️ Run the following to generate the Directive and the directive’s tests:
ng g directive onlyNumbers
How to verify your solution is correct
✏️ Update the spec file src/app/only-numbers.directive.spec.ts to be:
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { OnlyNumbersDirective } from './only-numbers.directive';
const simulateTyping = <T>(
debugElement: DebugElement,
fixture: ComponentFixture<T>,
value: string
) => {
let buildString = '';
for (const singleValue of value) {
const keydownEvent = new KeyboardEvent('keydown', {
key: singleValue,
cancelable: true,
});
debugElement.nativeElement.dispatchEvent(keydownEvent);
if (!keydownEvent.defaultPrevented) {
buildString = `${buildString}${singleValue}`;
}
}
debugElement.nativeElement.value = buildString;
debugElement.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
};
@Component({
template: `<input name="phone" type="text" pmoOnlyNumbers />`,
})
class TestInputComponent {}
describe('OnlyNumbersDirective', () => {
let debugElement: DebugElement;
let fixture: ComponentFixture<TestInputComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [OnlyNumbersDirective, TestInputComponent],
imports: [FormsModule],
providers: [],
}).createComponent(TestInputComponent);
fixture.detectChanges();
debugElement = fixture.debugElement.query(By.css('input'));
});
it('should create an instance', () => {
const directive = new OnlyNumbersDirective(debugElement);
expect(directive).toBeTruthy();
});
it('should contain only text 329053', () => {
const inputString = '32T90V53CFACR';
simulateTyping(debugElement, fixture, inputString);
expect(debugElement.nativeElement.value).toEqual('329053');
});
it('should contain only text 200', () => {
const inputString = 'THisIsA200LETTERWORD';
simulateTyping(debugElement, fixture, inputString);
expect(debugElement.nativeElement.value).toEqual('200');
});
it('should be an empty string', () => {
const inputString = 'STRING OF LETTER NO NUMBER';
simulateTyping(debugElement, fixture, inputString);
expect(debugElement.nativeElement.value).toBe('');
});
});
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Solution
Click to see the solution
✏️ Update src/app/only-numbers.directive.ts to:import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[pmoOnlyNumbers]',
})
export class OnlyNumbersDirective {
private allowedKeys: string[] = ['Backspace', 'ArrowLeft', 'ArrowRight'];
private regExp: RegExp = new RegExp(/^[0-9]*$/g);
constructor(private elementRef: ElementRef) {}
@HostListener('keydown', ['$event'])
onKeyDown(keyboardEvent: KeyboardEvent) {
if (this.allowedKeys.indexOf(keyboardEvent.key) !== -1) {
return;
}
const inputNativeElementValue = this.elementRef.nativeElement.value;
const next = `${inputNativeElementValue}${keyboardEvent.key}`;
if (next && !next.match(this.regExp)) {
keyboardEvent.preventDefault();
}
}
}