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.
Related
I mocked up a very small example of my problem here: https://github.com/lovefamilychildrenhappiness/AngularCustomComponentValidation
I have a custom component, which encapsulates an input field. The formControl associated with this input field has Validators.required (it is a required field). Inside the custom component, I have an onChange event which is fired when text is entered. I check if field is empty; if so, I add css class using ngClass. I also have set the registerOnChange of NG_VALUE_ACCESSOR, so I notify the form when the input changes. Finally, I implement NG_VALIDATORS interface to make the formControl invalid or valid.
My problem is I have a button that is clicked (it's not the submit button). When this button is clicked, I need to check if the custom component is blank or not, and if it is, change the css class and make the form invalid. I think the validate method of NG_VALIDATORS is doing that. But I need to change the css class of customComponent so background turns red. I spend severals hours on this and cannot figure it out:
// my-input.component.html
<textarea
[value]="value"
(input)="onChange($event.target.value)"
[ngClass]="{'failed-validation' : this.validationError }">
</textarea>
// my-input.component.ts
validate(control: FormControl): ValidationErrors | null {
if(!this.validationError){
return null
} else {
return { required: true };
}
}
private onChange(val) {
if(val.length > 0) {
this.value = val
this.validationError = false;
} else {
this.validationError = true;
}
// update the form
this.propagateChange(val);
}
// app.component.html
<form [formGroup]="reactiveForm">
<app-my-input formControlName="result"></app-my-input>
<input
value="Submit"
(click)="nextStep($event)"
type="button">
</form>
// app.component.ts
private nextStep(event){
// How do I dynamically change the class of the form control so I can change the style if formControl invalid when clicking the nextStep button
// pseudocode:
// if( !this.reactiveForm.controls['result'].valid ){
// this.reactiveForm.controls['result'].addClass('failed-validation');
// }
}
How can I get the css of the formControl to change in another component?
Since you using reactive form I have modified your custom form control. Here I have Use Injected NgControl Which is base class for all FormControl-based directives extend.
Try this:
import { Component, Input, forwardRef, OnInit } from "#angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NgControl,
NG_VALIDATORS,
FormControl,
ValidationErrors,
Validator
} from "#angular/forms";
#Component({
selector: "app-my-input",
templateUrl: "./my-input.component.html",
styleUrls: ["./my-input.component.scss"]
})
export class MyInputComponent implements ControlValueAccessor, OnInit {
private propagateChange = (_: any) => {};
value = "";
onTouch: () => void;
constructor(public controlDir: NgControl) {
controlDir.valueAccessor = this;
}
writeValue(value) {
this.value = value;
}
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched(fn) {
this.onTouch = fn;
}
onChange(value) {
this.propagateChange(value);
}
ngOnInit() {
const control = this.controlDir.control;
control.setValidators([control.validator ? control.validator : null]);
control.updateValueAndValidity();
}
}
Example
For More Information Forms Check this
I am trying to use the ngx-mat-select-search component to put a mat-select style dropdown menu with a search bar in my application.
https://www.npmjs.com/package/ngx-mat-select-search
I have the dropdown working fine, but I am trying to turn it into a custom directive that I can then call and reuse on multiple pages through out the app.
So far I have this: site-dropdown-component.ts
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '#angular/core';
import {FormControl} from '#angular/forms';
import {ReplaySubject, Subject} from 'rxjs';
import {MatSelect} from '#angular/material';
import {take, takeUntil} from 'rxjs/operators';
#Component({
selector: 'app-site-dropdown',
template: `
<mat-form-field class="w-100">
<mat-select [formControl]="siteCtrl" placeholder="Site" #singleSelect>
<mat-option>
<ngx-mat-select-search [formControl]="siteFilterCtrl" [placeholderLabel]="'Search Sites...'"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let site of filteredSites | async" [value]="site">{{site.name}}</mat-option>
</mat-select>
</mat-form-field>
`
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit {
/** list of sites */
protected sites: Site[] = SITES;
/** control for the selected site */
public siteCtrl: FormControl = new FormControl();
/** control for the MatSelect filter keyword */
public siteFilterCtrl: FormControl = new FormControl();
/** list of sites filtered by search keyword */
public filteredSites: ReplaySubject<Site[]> = new ReplaySubject<Site[]>(1);
#ViewChild('singleSelect') singleSelect: MatSelect;
/** Subject that emits when the component has been destroyed. */
protected onDestroy = new Subject<void>();
constructor() { }
ngOnInit(): void {
// set initial selection
this.siteCtrl.setValue(this.sites);
// load the initial site list
this.filteredSites.next(this.sites.slice());
// listen for search field value changes
this.siteFilterCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(() => {
this.filterSites();
});
}
ngAfterViewInit(): void {
this.setInitialValue();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
/**
* Sets the initial value after the filteredBanks are loaded initially
*/
protected setInitialValue() {
this.filteredSites
.pipe(take(1), takeUntil(this.onDestroy))
.subscribe(() => {
// setting the compareWith property to a comparison function
// triggers initializing the selection according to the initial value of
// the form control (i.e. _initializeSelection())
// this needs to be done after the filteredBanks are loaded initially
// and after the mat-option elements are available
this.singleSelect.compareWith = (a: Site, b: Site) => a && b && a.id === b.id;
});
}
protected filterSites() {
if (!this.sites) {
return;
}
// get the search keyword
let search = this.siteFilterCtrl.value;
if (!search) {
this.filteredSites.next(this.sites.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the sites
this.filteredSites.next(
this.sites.filter(site => site.name.toLowerCase().indexOf(search) > -1)
);
}
}
export interface Site {
id: string;
name: string;
}
export const SITES: Site[] = [
{id: 'site1', name: 'Site 1'},
{id: 'site2', name: 'Site 2'},
{id: 'site3', name: 'Site 3'},
];
For the component im trying to use it in, i have:
<app-site-dropdown formControlName="site"></app-site-dropdown>
And inside the component class I have a form:
this.mySearchForm = this.formBuilder.group( {
site: []
});
I can see and interact with the dropdown just fine, but when i submit my form, I cannot get the value of the selected option. It just always returns null when i try mySearchForm.controls['site'].value
What am I missing to be able to inject my custom dropdown component, and retrieve its value upon form submission?
UPDATE:
I was able to make it work by doing the following:
Inside site-dropdown.component.ts, I changed
protected siteCtrl: FormControl;
to
#Input() siteCtrl: FormControl;
And inside my html using the custom dropdown, i added:
<app-site-dropdown [siteCtrl]="myForm.get('site')"></app-site-dropdown>
This allowed me to save the selected value into my form on submission.
you can get the value of the selected option by having your SiteDropdownComponent implement the ControlValueAccessor interface as follows, resulting in your SiteDropdownComponent behaving as a form control and allowing to access the value with e.g. <app-site-dropdown formControlName="site"></app-site-dropdown>:
...
import { forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'app-site-dropdown',
template: ...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SiteDropdownComponent),
multi: true
}
],
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
...
onChange: Function = (_: any) => {};
onTouched: Function = (_: any) => {};
constructor() { }
ngOnInit() {
...
// call this.onChange to notify the parent component that the value has changed
this.siteCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(value => this.onChange(value))
}
writeValue(value: string) {
// set the value of siteCtrl when the value is set from outside the component
this.siteCtrl.setValue(value);
}
registerOnChange(fn: Function) {
this.onChange = fn;
}
registerOnTouched(fn: Function) {
this.onTouched = fn;
}
}
See e.g. https://github.com/bithost-gmbh/ngx-mat-select-search/blob/d7ea78d511bbec45143c58c855f013a44d0d5055/src/app/mat-select-search/mat-select-search.component.ts#L134
I would like to create a directive that when it's given an variable with value of TRUE and if the user change the input value it won't write out that new value to the NgModel.
Example of use:
Directive selector: d-avoid-change
<input type="text" name="surname"
[(ngModel)]="model.surname"
[d-avoid-change]="true">
If the surname model that came from the server is "Foo" and the user change it to "Bar", the model.surname stays "Foo" and I can give a message to the user.
I was trying with another approach with the directive, that was to remove the input and click EventListener so the user would not be able to click, but that would seem like a bug.
I want to use it instead of the disabled property of HTML, because if I just use [disabled]="true" the user can open browser HTML inspector and change the value and save it, also I don't want to validate those permissions on the server. I've searched alot about this and couldn't find any suggestion, does anyone know how I could do that?
Found a way to build a directive that saves the original NgModel in a private variable and then if it receives the Input parameter as TRUE it will ignore the changing and put the original NgModel instead.
Also it overrides the native disabled so I don't need to use the directive and also the native disabled.
Directive code:
import {
Directive,
ElementRef,
AfterViewInit,
Input,
AfterContentInit,
ViewContainerRef,
Renderer2
} from '#angular/core';
import { NgModel } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
declare let $;
#Directive({
selector: '[d-disabled]',
providers: [NgModel]
})
export class DisabledDirective implements AfterViewInit {
#Input('d-disabled')
set disabled(disabled: boolean) {
if (disabled) {
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
} else {
this.renderer.removeAttribute(this.el.nativeElement, 'disabled');
}
this._disabled = disabled;
}
_disabled: boolean;
originalModel: any;
constructor(private el: ElementRef,
private ngModel: NgModel,
private renderer: Renderer2) {
this.ngModel.valueAccessor.registerOnChange = this.registerOnChange;
this.ngModel.valueAccessor.registerOnTouched = this.registerOnTouched;
this.originalModel = this.ngModel;
}
ngAfterViewInit() {
Observable.fromEvent(this.el.nativeElement, 'input')
.map((n: any) => n.target.value)
.subscribe(n => {
if (this._disabled) {
this.ngModel.viewToModelUpdate(this.originalModel.value);
this.ngModel.control.patchValue(this.originalModel.value);
this.ngModel.control.updateValueAndValidity({ emitEvent: true });
} else {
this.onChangeCallback(n);
}
});
}
private onChangeCallback: (_: any) => void = (_) => { };
private onTouchedCallback: () => void = () => { };
registerOnChange = (fn: (_: any) => void): void => { this.onChangeCallback = fn; };
registerOnTouched = (fn: () => void): void => { this.onTouchedCallback = fn; };
}
How to use it:
<input type="text" name="surname"
[(ngModel)]="model.surname"
[d-disabled]="true">
If anyone can help me to improve in any way this method, but this is working as I wanted.
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
Consider an Angular 2 component that has an #Input meant to be a boolean, where its presence indicates true and its absence indicates false. Right now, I might manage this either with a custom getter/setter, or by having a function that tells me whether the attribute is present:
#Component({
selector:'foo',
template:`<div [class.error]="hasError()">Hello world</div>`
})
class Foo {
#Input() error:string;
public hasError() {
return error === '' || !!error;
}
}
And then I can use it in another component like this:
<foo></foo> <!-- no error class -->
<foo error></foo> <!-- has error class -->
What I really want to do is this, but have the same behavior:
#Component({
selector:'foo',
template:`<div [class.error]="error">Hello world</div>`
})
class Foo {
#Input() error:boolean;
}
Is there a common pattern for creating this behavior without the boilerplate in my first example?
What you need is a decorator that wraps your boolean property as a getter/setter and handles all the logic.
It's quite simple and save that boilerplate.
This feature is already implemented by the material team in google, they build the material library for Angular 2 and they work closely with the angular team.
Currently it's implemented in their repo, not in the angular repo but if high demand will raise I suppose they might consider migrating it to angular, it has been done in some cases before.
Anyway, this is dead simple and it's around 15-20 LOC.
/**
* Annotation Factory that allows HTML style boolean attributes. For example,
* a field declared like this:
* #Directive({ selector: 'component' }) class MyComponent {
* #Input() #BooleanFieldValueFactory() myField: boolean;
* }
*
* You could set it up this way:
* <component myField>
* or:
* <component myField="">
*/
function booleanFieldValueFactory() {
return function booleanFieldValueMetadata(target: any, key: string): void {
const defaultValue = target[key];
const localKey = `__md_private_symbol_${key}`;
target[localKey] = defaultValue;
Object.defineProperty(target, key, {
get() { return (<any>this)[localKey]; },
set(value: boolean) {
(<any>this)[localKey] = value != null && `${value}` !== 'false';
}
});
};
}
export { booleanFieldValueFactory as BooleanFieldValue };
You can see the implementation in THIS LINK
This works for me (Angular 8.2):
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements OnInit {
#Input() inverse = false;
ngOnInit() {
this.inverse = !(this.inverse === false);
}
}
If you want more than a presence/absence check and really check for empty string, you can do that, too:
this.inverse = (this.inverse === "");