27

I want to change (force) input field values while typing using a attribute Directive. With it I would like to create directives like uppercase, lowercase, maxlength, filterchar, etc. to be used on input fields on forms. I found this example: Angular 2 Attribute Directive Typescript Example but this doesn't seem to work. Maybe it did for an earlier build of Angular2. It is however exactly what I would like to do.

When I create a directive like this:

import {Directive} from 'angular2/core';
import {NgModel} from 'angular2/common';

@Directive({ 
selector: '[ngModel][uppercase]', 
host: {
    '(input)' : 'onInputChange()'
      }
})
export class UppercaseDirective{

constructor(public model:NgModel){}

onInputChange(){
    var newValue = this.model.value.toUpperCase();
    this.model.valueAccessor.writeValue(newValue);
    this.model.viewToModelUpdate(newValue);
    }
}

And use it on a form like this:

<input type="text" class="form-control" [(ngModel)]="field.name" ngControl="name" #name="ngForm" required uppercase>

(and register NgModel as a provider). I get an

undefined this.model.value.

I can use $event.target.value = $event.target.value.toUpperCase() (when passing $event with the onInputChange()) and that works for the view (it does show the input as uppercase. But it doesn't update the bind field "field.name".

So how to create an Angular2 attribute directive that does this?

-- EDIT --

After some further investigation I managed to get what I want. The answer Günter provided is closer to my original intention and perhaps better. But here is another way:

import {Directive, Input, Output, EventEmitter} from 'angular2/core';

@Directive({ 
selector: '[ngModel][uppercase]',
host: {
"(input)": 'onInputChange($event)'
    }
})
export class UppercaseDirective{
@Output() ngModelChange:EventEmitter<any> = new EventEmitter()
value: any

onInputChange($event){
    this.value = $event.target.value.toUpperCase()
    this.ngModelChange.emit(this.value)
    }
}

As I said I'm not sure if this is also a good way to do this so comments are welcome.

majodi
  • 536
  • 1
  • 7
  • 17

4 Answers4

38

Although Günter's answer looks promising, there is a bug in that the final value in the model has the last entered letter in lowercase.

See here:

https://plnkr.co/edit/SzxO2Ykg2pKq1qfgKVMH

Please use the answer provided in the question. It works correctly.

@Directive({ 
selector: '[ngModel][uppercase]',
host: {
"(input)": 'onInputChange($event)'
    }
})
export class UppercaseDirective{
@Output() ngModelChange:EventEmitter<any> = new EventEmitter()
value: any

onInputChange($event){
    this.value = $event.target.value.toUpperCase()
    this.ngModelChange.emit(this.value)
    }
}

https://plnkr.co/edit/oE3KNMCG7bvEj8FV07RV

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Ryan How
  • 1,027
  • 1
  • 9
  • 6
  • My Plunker seems to work fine. How can I reproduce the issue you mention? – Günter Zöchbauer Jun 01 '16 at 06:53
  • 1
    In your plunker, if you put a binding eg.

    Hello {{field.name}}

    . It has a lowercase last letter.
    – Ryan How Jun 02 '16 at 14:41
  • 2
    Also, in angular rc1 updating the model triggers the ngModelChange event to trigger again and you get an infinite loop. – Ryan How Jun 02 '16 at 14:47
  • 1
    And in respect to the original question (which I can't comment on :/), in rc1 the initial code would work if using $event.target.value.toUpperCase() because updating the model emits the model change event. Seems a few things have changed! – Ryan How Jun 02 '16 at 14:56
  • 1
    @RyanHow: The plnkrs are both not running for me. I experimented with that approach, and it's working. But what can I do, if I want to have original input values in input field, but modified values in model? With this approach I still have "feedback" from the model back to the input. – westor Jul 13 '16 at 13:53
  • 1
    @westor: Not sure why they aren't working. They don't even load for me now :(. You can use getters and setters on your model to give the effect of formatters and parsers which angular2 supports out of the box. Not sure how to do it from an attribute directive though. I have the same problem but haven't looked into it since I got this part working. Need some experts here hey! – Ryan How Jul 15 '16 at 05:17
  • @RyanHow it works for me partially. it updated my textbox value but it also gives me an error 'ExpressionChangedAfterItHasBeenCheckedError' could pls tell me why this would probably be occurred – MeVimalkumar Mar 05 '18 at 14:03
  • 2
    @Vicky Sorry, Angular has changed so much since here and I suspect this doesn't work anymore. – Ryan How Mar 06 '18 at 15:13
13

update

This approach doesn't work properly. See @RyanHow's answer for a better solution.

original

@Directive({ 
  selector: '[ngModel][uppercase]',
  providers: [NgModel],
  host: {
    '(ngModelChange)' : 'onInputChange($event)'
  }
})
export class UppercaseDirective{
  constructor(private model:NgModel){}

  onInputChange(event){
    this.model.valueAccessor.writeValue(event.toUpperCase());
  }
}

Plunker

Alex Okrushko
  • 7,212
  • 6
  • 44
  • 63
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    This is exactly what I was trying to do Günter, thnx! This really makes sense. In the mean time I found another way of doing this. I will edit my question with this variation. I'm not sure if this is also a good way. Maybe you could have a look at it. – majodi Mar 19 '16 at 22:37
  • Sure, just write a comment after you added your answer, so I get notified. – Günter Zöchbauer Mar 19 '16 at 22:38
  • I tried the `@Output() ngModelChange:EventEmitter` to set the value but this didn't work for me :D. I think using `ngModelChange` for the `@Input()` has the advantage that it works for all kinds of input elements that are covered by `ngModel` and also with browsers where different events are used (there are currently issues with select and radio inputs because of this - at least when the `ngModel` issues are fixed. I guess I'd like a combination of my `@Input()` and your `@Output()` best if it is actually working. – Günter Zöchbauer Mar 19 '16 at 22:53
  • I tried the combination but that causes endless loops :-/ – Günter Zöchbauer Mar 19 '16 at 22:55
  • 1
    I will experiment a bit more using both options. For now I'm happy to have learned enough to get it right somehow. thnx. – majodi Mar 19 '16 at 23:02
  • http://stackoverflow.com/questions/35826325/how-to-convert-input-value-to-uppercase-in-angular-2-value-passing-to-ngcontrol – Kody Mar 03 '17 at 21:37
  • 1
    Thanks for your help .. nice solution :) – Abhijeet Mar 16 '18 at 11:59
1

I have faced the same issue, where I need to create the custom select in Angular with select2. I have created following directive thing to achieve this with attribute directive and ngModel.

import {ElementRef, Directive, EventEmitter, Output, Input} from '@angular/core';
import {NgModel} from "@angular/forms";
declare let $;
@Directive({
  selector: '[custom-select]',
  providers: [NgModel]
})
export class CustomSelectComponent{
  $eventSelect:any;
  @Output() ngModelChange:EventEmitter<any> = new EventEmitter();
  @Input() set ngModel(value:any){
    //listen to the input value change of ngModel and change in the plugin accordingly.
    if(this.$eventSelect){
      this.$eventSelect.val(value).trigger('change',{fromComponent:true});
    }
  }
  constructor(private elementRef: ElementRef) {}
  ngOnInit(){
    this.$eventSelect = $(this.elementRef.nativeElement);
    this.$eventSelect.select2({minimumResultsForSearch:-1});
    this.$eventSelect.on("change.select2", (event,data)=> {
      //listen to the select change event and chanage the model value 
      if(!data || !data.fromComponent){ //dont change model when its chagned from the input change event
        this.ngModelChange.emit(this.$eventSelect.val());
      }
    });
  }
}

with following usage

<select custom-select [(ngModel)]="protocol.type">
  <option value="1">option1</option>
  <option value="1">option2</option>
</select>
Dinesh Dabhi
  • 856
  • 1
  • 7
  • 18
1

The requirement I had to create a directive to trim the leading and trailing spaces for text input. my solution:

import { Directive, ElementRef, HostListener, Output, EventEmitter } from '@angular/core';
import { NgModel } from "@angular/forms";

@Directive({
    selector: '[text-trim]',
    providers: [NgModel]
})

export class TextInputTrimDirective {

@Output() ngModelChange: EventEmitter<any> = new EventEmitter();

constructor(private el: ElementRef) {}

@HostListener('change') onInputChange() {
    const value = this.el.nativeElement.value.trim();
    this.ngModelChange.emit(value);
}
}
Ali Jamal
  • 619
  • 7
  • 12