Creating Directive 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 Directives are
- What ElementRef is
- What HostListeners are
- 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 Directives
- Attribute Directives
- Structural Directives
Components Directives
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.
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. Read more.
Structural Directives
Structural Directives are types of Directives that are used to change HTML DOM layout by adding, removing or manipulating Elements. Read more
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.
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.
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" [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, when you run
npm run test
all tests will pass!
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();
}
}
}