I have developed a custom directive which trims value of input controls.
Please find the code for the same:
import { Directive, HostListener, Provider } from '#angular/core';
import { NgModel } from '#angular/forms';
#Directive({
selector: '[ngModel][trim]',
providers: [NgModel],
host: {
'(ngModelChange)': 'onInputChange($event)',
'(blur)': 'onBlur($event)'
}
})
export class TrimValueAccessor {
onChange = (_) => { };
private el: any;
private newValue: any;
constructor(private model: NgModel) {
this.el = model;
}
onInputChange(event) {
this.newValue = event;
console.log(this.newValue);
}
onBlur(event) {
this.model.valueAccessor.writeValue(this.newValue.trim());
}
}
The problem is ngModel not updating value on onBlur event.
I tried to trim value on onModelChange event but it doesn't allow space between two words(e.g., ABC XYZ)
Any suggestion would be helpful.
Please add below lines of code in onblur event instead of existing code.It would work:
this.model.control.setValue(this.newValue.trim());
Thanks!
Related
So, I was just minding my own business on Gitter when I came across something interesting. Take a look at this Stackblitz demo.
It uses a custom directive that makes a custom checkbox component work with ngModel/formControl etc.
What I don't understand here is how the events set for host in the NgCheckboxControlDirective work. Since the host here is the selector for the checkbox component and not the checkbox input itself, it shouldn't trigger any change or blur event.. but it does.
If you click on the checkbox in the demo linked at the top you'll see that choosen changes between true and false.
Why? Why does clicking on the checkbox fire a change event here which then updates the choosen state?
The directive which implements ControlValueAccessor:
import { Directive, Renderer2, ElementRef } from '#angular/core';
import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '#angular/forms';
#Directive({
selector: '[ngCheckboxControl]',
host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'},
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: NgCheckboxControlDirective,
multi: true
}
]
})
export class NgCheckboxControlDirective implements ControlValueAccessor {
onChange = (_: any) => {};
onTouched = () => {};
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this._renderer.setProperty(this._elementRef.nativeElement.firstElementChild, 'checked', value);
}
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement.firstElementChild, 'disabled', isDisabled);
}
}
The checkbox:
import { Component, ElementRef } from '#angular/core';
import { NgControl } from '#angular/forms';
#Component({
selector: 'checkbox-layout',
template: `
My checkbox: <input type="checkbox" />
`,
})
export class CkeckboxLayoutComponent {
constructor(
private ngControl: NgControl,
private elementRef: ElementRef
) {
console.log('... elementRef:', elementRef);
console.log('... ngControl:', ngControl);
}
}
Then you'd use it like this:
<checkbox-layout formControlName="choosen" ngCheckboxControl></checkbox-layout>
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 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.
I have some problems to catch the change event when I use the jQuery datepicker plugin and I'm trying to use the (change) method to catch the change but seems that when I'm using this plugin, angular can't catch it.
#Component({
selector: 'foo-element',
template: '<input type="text" (change)="checkDates($event)" id="foo_date_picker" class="datepicker">'
})
export class FooComponentClass implements AfterViewInit {
ngAfterViewInit():any{
$('#end_day').datepicker();
}
private function checkDates(e){
console.log("Please, catch the change event ): ");
}
}
I have removed the datepicker initialization and works fine, but when I use it again... don't works.
Someone can help me!
Thanks so much.
You could implement the following directive:
#Directive({
selector: '[datepicker]'
})
export class DatepickerDirective {
#Output()
change:EventEmitter<string> = new EventEmitter();
constructor(private elementRef:ElementRef) {
}
ngOnInit() {
$(this.elementRef.nativeElement).datepicker({
onSelect: (dateText) => {
this.change.emit(dateText);
}
});
}
}
This way you will be able to catch a change event like this:
#Component({
selector: 'app',
template: '<input type="text" id="end_day" (change)="checkDates($event)" class="datepicker" datepicker>',
directives: [ DatepickerDirective ]
})
export class App implements AfterViewInit {
checkDates(e){
console.log("Please, catch the change event ): "+e);
}
}
See this plunkr: https://plnkr.co/edit/TVk11FsItoTuNDZLJx5X?p=preview
I'm looking to update an Angular 1 app to Angular 2 and am having an issue with one of my old directives.
The idea is simple. When an input field is focused a class should be added (md-input-focus) and another be removed (md-input-wrapper). Then this process should be reversed on "blur" event - i.e. focus lost.
My old directive simply included the lines
.directive('mdInput',[
'$timeout',
function ($timeout) {
return {
restrict: 'A',
scope: {
ngModel: '='
},
link: function (scope, elem, attrs) {
var $elem = $(elem);
$elem.on('focus', function() {
$elem.closest('.md-input-wrapper').addClass('md-input-focus')
})
.on('blur', function() {
$(this).closest('.md-input-wrapper').removeClass('md-input-focus');
})
}
etc...
I obviously have the classic start to my directive but have run out of.....skill
import {Directive, ElementRef, Renderer, Input} from 'angular2/core';
#Directive({
selector: '.mdInput',
})
export class MaterialDesignDirective {
constructor(el: ElementRef, renderer: Renderer) {
// Insert inciteful code here to do the above
}
}
Any help would be appreciated.
UPDATE:
The HTML would look like (before the input element was focused):
<div class="md-input-wrapper">
<input type="text" class="md-input">
</div>
and then
<div class="md-input-wrapper md-input-focus">
<input type="text" class="md-input">
</div>
after.
The input element is the only one which can receive a focus event (and therefore the target for the directive) however the parent <div> requires the class addition and removal.
Further help
Please see Plunker for help/explanation - would be great if someone could help
Update
#Directive({selector: '.md-input', host: {
'(focus)': 'setInputFocus(true)',
'(blur)': 'setInputFocus(false)',
}})
class MaterialDesignDirective {
MaterialDesignDirective(private _elementRef: ElementRef, private _renderer: Renderer) {}
setInputFocus(isSet: boolean): void {
this.renderer.setElementClass(this.elementRef.nativeElement.parentElement, 'md-input-focus', isSet);
}
}
Original
This can easily be done without ElementRef and Renderer (what you should strive for in Angular2) by defining host bindings:
import {Directive, ElementRef, Renderer, Input} from 'angular2/core';
#Directive({
selector: '.mdInput',
host: {
'(focus)':'_onFocus()',
'(blur)':'_onBlur()',
'[class.md-input-focus]':'inputFocusClass'
}
})
export class MaterialDesignDirective {
inputFocusClass: bool = false;
_onFocus() {
this.inputFocusClass = true;
}
_onBlur() {
this.inputFocusClass = false;
}
}
or a bit more terse
#Directive({
selector: '.mdInput',
host: {
'(focus)':'_setInputFocus(true)',
'(blur)':'_setInputFocus(false)',
'[class.md-input-focus]':'inputFocusClass'
}
})
export class MaterialDesignDirective {
inputFocusClass: bool = false;
_setInputFocus(isFocus:bool) {
this.inputFocusClass = isFocus;
}
}
I tried it only in Dart where it works fine. I hope I translated it correctly to TS.
Don't forget to add the class to the directives: of the element where you use the directive.
In addition to previous answers, if you don't want to add a directive, for the specific component (you already have a directive for a parent component, you are using Ionic 2 page or something else), you inject the renderer by adding private _renderer: Renderer in the page constructor and update the element using the event target like this:
html:
(dragstart)="dragStart($event)"
TS:
dragStart(ev){
this._renderer.setElementClass(ev.target, "myClass", true)
}
Edit: to remove the class just do:
dragEnd(ev){
this._renderer.setElementClass(ev.target, "myClass", false)
}
The name of the selector has to be inside "[ ]", as shown below
#Directive({
selector: '[.mdInput]',
host: {
'(focus)':'_setInputFocus(true)',
'(blur)':'_setInputFocus(false)',
'[class.md-input-focus]':'inputFocusClass'
}
})
If you want to catch the focus / blur events dynamiclly on every input on your component :
import { AfterViewInit, Component, ElementRef } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements AfterViewInit {
name = 'Angular focus / blur Events';
constructor(private el: ElementRef) {
}
ngAfterViewInit() {
// document.getElementsByTagName('input') : to gell all Docuement imputs
const inputList = [].slice.call((<HTMLElement>this.el.nativeElement).getElementsByTagName('input'));
inputList.forEach((input: HTMLElement) => {
input.addEventListener('focus', () => {
input.setAttribute('placeholder', 'focused');
});
input.addEventListener('blur', () => {
input.removeAttribute('placeholder');
});
});
}
}
Checkout the full code here : https://stackblitz.com/edit/angular-wtwpjr