I have created a very simple directive to use on input elements, that should only allow entry of a decimal number (numeric with single decimal point).
The directive is defined as follows:
import { HostListener, Directive, ElementRef } from '#angular/core';
// Directive attribute to stop any input, other than a decimal number.
#Directive({
selector: '[decimalinput]'
})
export class DecimalInputDirective {
constructor(private element : ElementRef) { }
// Hook into the key press event.
#HostListener('keypress', ['$event']) onkeypress( keyEvent : KeyboardEvent ) : boolean {
// Check if a full stop already exists in the input.
var alreadyHasFullStop = this.element.nativeElement.value.indexOf('.') != -1;
// Get the key that was pressed in order to check it against the regEx.
let input = String.fromCharCode(keyEvent.which);
// Test for allowed character using regEx. Allowed is number or decimal.
var isAllowed = /^(\d+)?([.]?\d{0,2})?$/.test( input );
// If this is an invlid character (i.e. alpha or symbol) OR we already have a full stop, prevent key press.
if (!isAllowed || (isAllowed && input == '.' && alreadyHasFullStop)){
keyEvent.preventDefault();
return false;
}
return true;
}
}
This directive should allow "123.123" not "abc", nor "1.2.1". Now I want to test this directive, reading online, I've come up with this so far:
import { Component, OnInit, TemplateRef,DebugElement, ComponentFactory, ViewChild, ViewContainerRef } from '#angular/core';
import { TestBed, ComponentFixture } from '#angular/core/testing';
import { DecimalInputDirective } from './decimalinput.directive';
import { By } from '#angular/platform-browser';
#Component({
template: `<input type="text" name="txtDecimalTest" decimalinput>`
})
class TestDecimalComponent { }
describe('Directive: DecimalInputDirective', () => {
let component: TestDecimalComponent;
let fixture: ComponentFixture<TestDecimalComponent>;
let decimalInput: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestDecimalComponent]
});
fixture = TestBed.createComponent(TestDecimalComponent);
component = fixture.componentInstance;
decimalInput = fixture.debugElement.query(By.css('input[name=txtDecimalTest]'));
});
it('Entering email and password emits loggedIn event', () => {
// This sets the value (I can even put "abc" here and it will work.
decimalInput.nativeElement.value = "12345";
// But I am trying to initialize the keypress event, so the character is tested in a real world way when the user is using.
decimalInput.nativeElement.dispatchEvent(new KeyboardEvent("keypress", { key: "a" })); // Nothing happens here! This was my attempt...
// This
expect(decimalInput.nativeElement.value).toBe("12345");
});
});
You can see from the code, the line:
decimalInput.nativeElement.dispatchEvent(new KeyboardEvent...
Is my attempt to simulate keypresses, as if the user was inputting. If I simulated a, then b, then c, then 1, then 2, then 3, I'd expect the test to make sure the value is only "123" and its ignored "abc" in the way the directive works.
Two questions - 1) is this the correct test I should be doing? 2) Whats wrong with my code - why is the simulated key press doing nothing?
Thanks for any pointers in advance! :)
Normally directive are tested in such a way that its being used in real component. So you can create a fake component which will use your directive and you can test that component to handle your directive.
This is the most people suggest.
So in your test file create a fake directive
// tslint:disable-next-line:data
#Component({
selector: 'sd-test-layout',
template: `
<div sdAuthorized [permission]="'data_objects'">test</div>`
})
export class TestDecimalInputDirectiveComponent {
#Input() permission;
constructor() {
}
}
Then in your before each using TestBed create the component instance, and now You are ready to apply mouse events and test them in real situation
TestBed.configureTestingModule({
imports: [
HttpModule,
SharedModule
],
declarations: [
TestDecimalInputDirectiveComponent,
],
providers: [
{
provide: ElementRef,
useClass: MockElementRef
},
AuthorizedDirective,
]
}).compileComponents();
Just given you the hint. you can follow this link to get more information
Related
I want to add a custom unique validator that will validate that all label fields values are unique.
(I) When I change the form values, the value of this.form changes after it is passed in CustomValidator.uniqueValidator(this.form). How to fix this?
(II) Is there any way of doing this without using any package?
Note: Forms have default values on load. Here is the screenshot.
this.form = this.fb.group({
fields: this.fb.array([])
});
private addFields(fieldControl?) {
return this.fb.group({
label: [
{value: fieldControl ? fieldControl.label : '', disabled: this.makeComponentReadOnly}, [
Validators.maxLength(30), CustomValidator.uniqueValidator(this.form)
]],
isRequired: [
{value: fieldControl ? fieldControl.isRequired : false, disabled: this.makeComponentReadOnly}],
type: [fieldControl ? fieldControl.type : 'text']
});
}
static uniqueValidator(form: any): ValidatorFn | null {
return (control: AbstractControl): ValidationErrors | null => {
console.log('control..: ', control);
const name = control.value;
if (form.value.fields.filter(v => v.label.toLowerCase() === control.value.toLowerCase()).length > 1) {
return {
notUnique: control.value
};
} else {
return null;
}
};
}
in real life, username or email properties are checked to be unique. This will be very long answer I hope you can follow along. I will show how to check uniqueness of username.
to check the database, you have to create a service to make a request. so this validator will be async validator and it will be written in class. this class will be communicate with the service via the dependency injection technique.
First thing you need to setup HttpClientModule. in app.module.ts
import { HttpClientModule } from '#angular/common/http';
#NgModule({
declarations: [AppComponent],
imports: [BrowserModule, YourOthersModule , HttpClientModule],
providers: [],
bootstrap: [AppComponent],
})
then create a service
ng g service Auth //named it Auth
in this auth.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable({
providedIn: 'root',
})
export class AuthService {
constructor(private http: HttpClient) {}
userNameAvailable(username: string) {
// avoid type "any". check the response obj and put a clear type
return this.http.post<any>('https://api.angular.com/username', {
username:username,
});
}
}
now create a class ng g class UniqueUsername and in this class:
import { Injectable } from '#angular/core';
import { AsyncValidator, FormControl } from '#angular/forms';
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { AuthService } from './auth.service';
// this class needs to use the dependency injection to reach the http client to make an api request
// we can only access to http client with dependecny injection system
// now we need to decorate this class with Injectable to access to AuthService
#Injectable({
providedIn: 'root',
})
export class UniqueUsername implements AsyncValidator {
constructor(private authService: AuthService) {}
//this will be used by the usernamae FormControl
//we use arrow function cause this function will be called by a
//different context, but we want it to have this class' context
//because this method needs to reach `this.authService`. in other context `this.authService` will be undefined.
// if this validator would be used by the FormGroup, you could use
"FormGroup" type.
//if you are not sure you can use type "control: AbstractControl"
//In this case you use it for a FormControl
validate = (control: FormControl) => {
const { value } = control;
return this.authService.userNameAvailable(value).pipe(
//errors skip the map(). if we return null, means we got 200 response code, our request will indicate that username is available
//catchError will catch the error
map(() => {
return null;
}),
catchError((err) => {
console.log(err);
//you have to console the error to see what the error object is. so u can
// set up your logic based on properties of the error object.
// i set as err.error.username as an example. your api server might return an error object with different properties.
if (err.error.username) {
//catchError has to return a new Observable and "of" is a shortcut
//if err.error.username exists, i will attach `{ nonUniqueUsername: true }` to the formControl's error object.
return of({ nonUniqueUsername: true });
}
return of({ noConnection: true });
})
);
};
}
So far we handled the service and async class validator, now we implement this on the form. I ll have only username field.
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
import { UniqueUsername } from '../validators/unique-username';
#Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css'],
})
export class SignupComponent implements OnInit {
authForm = new FormGroup(
{
// async validators are the third arg
username: new FormControl(
'',
[
Validators.required,
Validators.minLength(3),
Validators.maxLength(20),
Validators.pattern(/^[a-z0-9]+$/),
],
// async validators are gonna run after all sync validators
successfully completed running because async operations are
expensive.
this.uniqueUsername.validate
),
},
{ validators: [this.matchPassword.validate] }
);
constructor(
private uniqueUsername: UniqueUsername
) {}
//this is used inside the template file. you will see down below
showErrors() {
const { dirty, touched, errors } = this.control;
return dirty && touched && errors;
}
ngOnInit(): void {}
}
Final step is to show the error to the user: in the form component's template file:
<div class="field">
<input formControl="username" />
<!-- this is where you show the error to the client -->
<!-- showErrors() is a method inside the class -->
<div *ngIf="showErrors()" class="ui pointing red basic label">
<!-- authForm.get('username') you access to the "username" formControl -->
<p *ngIf="authForm.get('username').errors.required">Value is required</p>
<p *ngIf="authForm.get('username').errors.minlength">
Value must be longer
{{ authForm.get('username').errors.minlength.actualLength }} characters
</p>
<p *ngIf="authForm.get('username').errors.maxlength">
Value must be less than {{ authForm.get('username').errors.maxlength.requiredLength }}
</p>
<p *ngIf="authForm.get('username').errors.nonUniqueUsername">Username is taken</p>
<p *ngIf="authForm.get('username').errors.noConnection">Can't tell if username is taken</p>
</div>
</div>
You could create a validator directive that goes on the parent element (an ngModelGroup or the form itself):
import { Directive } from '#angular/core';
import { FormGroup, ValidationErrors, Validator, NG_VALIDATORS } from '#angular/forms';
#Directive({
selector: '[validate-uniqueness]',
providers: [{ provide: NG_VALIDATORS, useExisting: UniquenessValidator, multi: true }]
})
export class UniquenessValidator implements Validator {
validate(formGroup: FormGroup): ValidationErrors | null {
let firstControl = formGroup.controls['first']
let secondControl = formgroup.controls['second']
// If you need to reach outside current group use this syntax:
let thirdControl = (<FormGroup>formGroup.root).controls['third']
// Then validate whatever you want to validate
// To check if they are present and unique:
if ((firstControl && firstControl.value) &&
(secondControl && secondControl.value) &&
(thirdContreol && thirdControl.value) &&
(firstControl.value != secondControl.value) &&
(secondControl.value != thirdControl.value) &&
(thirdControl.value != firstControl.value)) {
return null;
}
return { validateUniqueness: false }
}
}
You can probably simplify that check, but I think you get the point.
I didn't test this code, but I recently did something similar with just 2 fields in this project if you want to take a look:
https://github.com/H3AR7B3A7/EarlyAngularProjects/blob/master/modelForms/src/app/advanced-form/validate-about-or-image.directive.ts
Needless to say, custom validators like this are fairly business specific and hard to make reusable in most cases. Change to the form might need change to the directive. There is other ways to do this, but this does work and it is a fairly simple option.
I am trying to test the code in the ngOnInit method. The code watches for change in screen size for a navigation bar to resize down to mobile or to stay as a top bar. I have tried a for about a week and keep getting a slew of different errors when I test. I have left out some code for comp.component.ts as the other code is not necessary for this. I keep getting subscribe is not a method or Can't resolve all parameters for MediaChange: (?, ?, ?, ?). Any advice on how I can achieve writing a test for this or any resources you might suggest looking at to help me figure this out.
comp.component.ts
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs';
import { MediaChange, ObservableMedia } from '#angular/flex-layout';
#Component({
selector: 'app-comp',
templateUrl: './comp.component.html',
styleUrls: ['./comp.component.scss']
})
export class NavigationComponent implements OnInit {
isOpen: Boolean;
watcher: Subscription;
activeMediaQuery = "";
media: ObservableMedia;
constructor() {
this.isOpen = false;
}
ngOnInit(): void {
this.watcher = this.media.subscribe((change: MediaChange) => {
this.activeMediaQuery = change ? `'${change.mqAlias}' = (${change.mediaQuery})` : '';
this.isOpen = false;
});
}
navPressed(event, path): void {
this.navClick.emit(path);
if ( this.checkSize() ) this.toggle();
}
checkSize(): Boolean {
return this.activeMediaQuery.includes('xs') || this.activeMediaQuery.includes('sm');
}
}
comp.component.spec.ts
import { Component } from '#angular/core';
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { DebugElement } from '#angular/core';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { MatButtonModule, MatToolbarModule, MatIconModule } from '#angular/material';
import { CompComponent } from './comp.component';
import { Subscription } from 'rxjs';
import { MediaChange, ObservableMedia } from '#angular/flex-layout';
#Component({
selector: 'app-test-component-wrapper',
template: '<app-navigation [navItems]="clickables" (navClick)="handleNavClick($event)"></app-navigation>'
})
class TestWrapperComponent {
clickables = [
{ path: '/login', label: 'Login', onClick() {} }
];
}
describe('app testing', () => {
let component: CompComponent;
let fixture: ComponentFixture<TestWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatButtonModule,
MatToolbarModule,
MatIconModule,
BrowserAnimationsModule
],
declarations: [
TestWrapperComponent,
NavigationComponent
],
providers: [
ObservableMedia,
MediaChange,
Subscription
]
}).compileComponents();
fixture = TestBed.createComponent(TestWrapperComponent);
}));
it('should create and have Login label', () => {
// EDIT START
spyOn(ObservableMedia, 'prototype');
// EDIT END
expect(fixture).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => {
component = fixture.debugElement.children[0].componentInstance;
expect(component.navItems[0].label).toBe('Login');
});
});
});
EDIT: Added the 'EDIT' comment in the code with the code I have added. I am now getting the resolve all parameters for MediaChange: (?, ?, ?, ?) error which I think is forward progress from the subscribe error mentioned above.
Some observations:
ObservableMedia from flex-layout needs to be injected into your component to work. Details here
You aren't providing MediaChange or Subscription in the original component, so no need to in the TestBed either.
In the stackblitz below I had to make a few assumptions. Let me know if any of these are wrong, or just go ahead and update the stackblitz:
In your spec you imported CompComponent, but in comp.component.ts you defined NavigationComponent. Of the two I chose to use NavigationComponent.
navClick was missing from your code above, so I assumed it is an #Output from your component (since you emit a path to it).
navItems was also missing from the code above, but since you are testing it I assumed it was important and guessed it is an input to your component (again, just by the way you were using it).
You didn't include your template, so I mocked it very simply.
toggle was called from within navPressed, but didn't exist so I created it as an empty function.
Here is the stackblitz: https://stackblitz.com/edit/stackoverflow-q-53024049?file=app%2Fmy.component.spec.ts
To fix what you had: I made the changes above and mocked the ObservableMedia object passed in with the following:
let mockFlex = jasmine.createSpyObj({
subscribe: ({mqAlias: 'xs', mediaQuery: ''}),
isActive: true,
});
I also changed the providers array to the following:
providers: [
{ provide: ObservableMedia, useValue: mockFlex }
]
Check the stackblitz for all the details. As you can see there, the test now passes.
I am testing this html form:
<input #nhcInput type="text" class="form-control" name="nhc" id="field_nhc"
[(ngModel)]="paciente.nhc" maxlength="38" pattern="[0-9]+"/>
<div [hidden]="!(editForm.controls.nhc?.dirty && editForm.controls.nhc?.invalid)">
<small class="form-text text-danger"
[hidden]="!editForm.controls.nhc?.errors?.maxlength" jhiTranslate="entity.validation.maxlength" translateValues="{ max: 38 }">
This field cannot be longer than 38 characters.
</small>
TEH RESULT {{nhcInput.className}} //This line prints ng-valid/ dirty, touched correctly
I have this in my component:
paciente: Paciente = {nhc: '23423' } as Paciente;
it ('NHC cannot have more than 38 characters', async(() => {
comp.paciente.nhc = 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr' ;
console.log(fixture.nativeElement.querySelector('input[name="nhc"]').className);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input[name="nhc"]').className.includes('ng-invalid')).toEqual(true);
}));
Now I want to test the validity by checking the vaidator.
The console.log prints out only form-control without the type of validator, since it is not finding it.
I put a validator for this fild like this in my component:
#ViewChild('editForm') editForm: any;
editform.controls["nhc"].setValidators([ Validators.maxLength(38)]);
But this doesnt work. Am I doing soemthing wrong here?
Thanks!
Your issue comes from the fact that you don't do things in order and don't rely on the framework to make your tests.
I made a sandbox with a working test, feel free to look at it. Now for the explanation :
Let's start with the component :
#Component({
selector: 'hello',
template: `
<form #myForm="ngForm">
<input type="text" maxlength="20" name="patient" id="patient" [(ngModel)]="patient">
</form>
`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
#ViewChild('myForm') myForm: NgForm;
patient: string;
}
A very simple component with a template driven form and a basic validation and binding.
If you do this
ngOnInit() {
console.log(this.myForm.controls);
setTimeout(() => console.log(this.myForm.controls));
}
You will see both undefined and { patient: FormControl }. This happens because you don't wait for the view to be initialized before doing your tests. This means that the test can't find the form control, and hence can't pass.
Now for the test itself :
import { Component } from '#angular/core';
import { ComponentFixture, TestBed, async } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
import { HelloComponent } from './hello.component';
import { FormsModule } from '#angular/forms';
describe('HelloComponent', () => {
let fixture: ComponentFixture<HelloComponent>;
let component: HelloComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [HelloComponent]
});
fixture = TestBed.createComponent(HelloComponent);
component = fixture.debugElement.children[0].componentInstance;
fixture.detectChanges();
});
it('should be invalid with more than 20 chars', async(() => {
setTimeout(() => {
component.myForm.controls['patient'].setValue('ddddddddddddddddddddd'); // 21
fixture.detectChanges();
expect(component.myForm.control.invalid).toEqual(true);
});
}));
});
the start is very basic, test bed is configured and changes are detected.
Now comes the part where you test : you first must wait for the component to load with a timeout, then, you need to set the value on the form by using the framework :
component.myForm.controls['patient'].setValue('ddddddddddddddddddddd'); // 21
This inputs 21 d into the input, which is making it invalid. After that, you need to trigger the changes detection, and now you can make your expectation with
expect(component.myForm.control.invalid).toEqual(true);
This will take the form as a control, meaning it has all the properties and functions a FormControl has. Among these, you can find the invalid property, which states if your control is in an invalid state.
Again, I have to state that this kind of test is useless, because you basically try to see if the Framework is working properly. That's not your job, that's Angular team's job. If you want to test something, you should test that the form can't be submitted when it has an invalid state, which is a business rule (I guess) and prevents side effects (unlike this test).
I would like to have custom errors in my Angular2 app. Thus I have extended ErrorHandler in my component:
import { Component, ErrorHandler, OnInit } from '#angular/core';
import { GenericError } from './generic-error.component';
#Component({
selector: 'custom-error-handler',
templateUrl: 'app/error-handler/custom-error-handler.component.html?' + +new Date()
})
export class CustomErrorHandler extends ErrorHandler {
errorText: string;
constructor() {
super(false);
}
ngOnInit() {
this.errorText = 'Initial text!';
}
public handleError(error: any): void {
if (error.originalError instanceof GenericError) {
console.info('This is printed to console!');
this.errorText = "I want it to print this in the template!";
}
else {
super.handleError(error);
}
}
}
My template simply contains:
<span style="color:red">{{errorText}}</span>
First I see "Initial text!" in the template as set in ngOnInit. That's as expected.
I can then throw a new exception like this from a different component:
throw new GenericError();
and it hits the code with handleError and prints to console but it doesn't update my template errorText with:
"I want it to print this in the template!"
It's like it ignores my template, when inside the handleError function.
What could be the problem here?
* ADDED MORE INFORMATION *
I thought I should add some more information. So here is the module I made for CustomErrorHandler (maybe the problem is with the providers?):
import { NgModule, ErrorHandler } from '#angular/core';
import { CommonModule } from '#angular/common';
import { CustomErrorHandler } from './custom-error-handler.component';
#NgModule({
declarations: [
CustomErrorHandler
],
imports: [
CommonModule
],
exports: [
CustomErrorHandler
],
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler }
]
})
export class CustomErrorModule { }
There is indeed only one instance of the CustomErrorHandler (I checked with the Augury Chrome plugin).
For completeness, here is is the GenericError component:
export class GenericError {
toString() {
return "Here is a generic error message";
}
}
The solution was to add a service as suggested in the question's comment track. This way I can set the property in the component and eventually show it in the template.
I created the service, so that it has a function which takes one parameter. Injected the service, call the service's function from the handleError in the component function, and send the text I want in the template as the parameter. Then I use an observable, to get the text back to the component.
In the constructor of the component, I added this observer.
let whatever = this.cs.nameChange.subscribe((value) => {
setTimeout(() => this.errorText = value);
});
I needed to add the setTimeout, or else it would not update the template before the second time the observable was changed.
Phew! The Angular team should make this global exception handling easier in future releases.
I would like to build a directive that can mutate values being passed to and from an input, bound with ngModel.
Say I wanted to do a date mutation, every time the model changes, the mutator first gets to change the value to the proper format (eg "2017-05-03 00:00:00" is shown as "2017/05/03"), before ngModel updates the view. When the view changes, the mutator gets to change the value before ngModel updates the model (eg entering "2017/08/03" sets the model to "2017-08-03 00:00:00" [timestamp]).
The directive would be used like this:
<input [(ngModel)]="someModel" mutate="date:YYYY/MM/DD" />
My first instinct was to get a reference to the ControlValueAccessor and NgModel on the Host component.
import { Directive, ElementRef, Input,
Host, OnChanges, Optional, Self, Inject } from '#angular/core';
import { NgModel, ControlValueAccessor,
NG_VALUE_ACCESSOR } from '#angular/forms';
#Directive({
selector: '[mutate]',
})
export class MutateDirective {
constructor(
#Host() private _ngModel: NgModel,
#Optional() #Self() #Inject(NG_VALUE_ACCESSOR)
private _controlValueAccessor: ControlValueAccessor[]
){
console.log('mutute construct', _controlValueAccessor);
}
}
Then I realized that the Angular 2 Forms classes are complicated and I have no idea what I'm doing. Any ideas?
UPDATE
Based on the answer below I came up with the solution: see gist
Usage (requires Moment JS):
<input mutate="YYYY/MM/DD" inputFormat="YYYY-MM-DD HH:mm:ss" [(ngModel)]="someDate">
Short answer: you need to implement ControlValueAccessor in some class and provide it as a NG_VALUE_ACCESSOR for the ngModel with some directive. This ControlValueAccessor and directive can actually be the same class.
TL;DR
It's not very obvious but still not very complicated. Below is the skeleton from one of my date controls. This thing acts as a parser/formatter pair for the angular 1 ng-model.
It all starts with ngModel injecting all NG_VALUE_ACCESSOR's into itself. There are bunch of default providers as well, and they all get injected into ngModel constructor, but ngModel can distinguish between default value accessors and the ones provided by the user. So it picks one to work with. Roughly it looks like this: if there's user's value accessor then it will be picked, otherwise it falls back to choosing from default ones. After that initial setup is done.
Control value accessor should subscribe to the 'input' or some other similar event on input element to process input events from it.
When value is changed externally ngModel calls writeValue() method on value accessor picked during initialization. This method is responsible for rendering display value that will go into an input as string shown to user.
At some point (usually on blur event) control can be marked as touched. This is shown as well.
Please note: code below is not real production code, it has not been tested, it can contain some discrepancies or inaccuracies, but in general it shows the whole idea of this approach.
import {
Directive,
Input,
Output,
SimpleChanges,
ElementRef,
Renderer,
EventEmitter,
OnInit,
OnDestroy,
OnChanges,
forwardRef
} from '#angular/core';
import {Subscription, Observable} from 'rxjs';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '#angular/forms';
const DATE_INPUT_VALUE_ACCESSOR_PROVIDER = [
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateInputDirective), multi: true}
];
#Directive({
// [date-input] is just to distinguish where exactly to place this control value accessor
selector: 'input[date-input]',
providers: [DATE_INPUT_VALUE_ACCESSOR_PROVIDER],
host: { 'blur': 'onBlur()', 'input': 'onChange($event)' }
})
export class DateInputDirective implements ControlValueAccessor, OnChanges {
#Input('date-input')
format: string;
model: TimeSpan;
private _onChange: (value: Date) => void = () => {
};
private _onTouched: () => void = () => {
};
constructor(private _renderer: Renderer,
private _elementRef: ElementRef,
// something that knows how to parse value
private _parser: DateParseTranslator,
// something that knows how to format it back into string
private _formatter: DateFormatPipe) {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['format']) {
this.updateText(this.model, true);
}
}
onBlur = () => {
this.updateText(this.model, false);
this.onTouched();
};
onChange = ($event: KeyboardEvent) => {
// the value of an input - don't remember exactly where it is in the event
// so this part may be incorrect, please check
let value = $event.target.value;
let date = this._parser.translate(value);
this._onChange(date);
};
onTouched = () => {
this._onTouched();
};
registerOnChange = (fn: (value: Date) => void): void => {
this._onChange = fn;
};
registerOnTouched = (fn: () => void): void => {
this._onTouched = fn;
};
writeValue = (value: Date): void => {
this.model = value;
this.updateText(value, true);
};
updateText = (date: Date, forceUpdate = false) => {
let textValue = date ? this._formatter.transform(date, this.format) : '';
if ((!date || !textValue) && !forceUpdate) {
return;
}
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', textValue);
}
}
Then in the html template:
<input date-input="DD/MM/YYYY" [(ngModel)]="myModel"/>
You shouldn't have to do anything with Forms here. As an example, I made a credit card masking directive that formats the user input into a credit card string (a space every 4 characters, basically).
import { Directive, ElementRef, HostListener, Input } from '#angular/core';
#Directive({
selector: '[credit-card]' // Attribute selector
})
export class CreditCard {
#HostListener('input', ['$event'])
confirmFirst(event: any) {
let val = event.target.value;
event.target.value = this.setElement(val);
}
constructor(public element: ElementRef) { }
setElement(val) {
let num = '';
var v = val.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
var matches = v.match(/\d{4,16}/g);
var match = matches && matches[0] || '';
var parts = [];
for (var i = 0, len = match.length; i < len; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
num = parts.join(' ').trim();
} else {
num = val.trim();
}
return num;
}
}
Then I used it in a template like so:
<input credit-card type="text" formControlName="cardNo" />
I am using form control in this example, but it doesnt matter either way. It should work fine with ngModel binding.