Attribute directive with ngModel to change field value
Asked Answered
C

4

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.

Chindwin answered 19/3, 2016 at 19:46 Comment(0)
A
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

Aalborg answered 19/3, 2016 at 20:12 Comment(7)
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.Chindwin
Sure, just write a comment after you added your answer, so I get notified.Until
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.Until
I tried the combination but that causes endless loops :-/Until
I will experiment a bit more using both options. For now I'm happy to have learned enough to get it right somehow. thnx.Chindwin
#35826825Trickster
Thanks for your help .. nice solution :)Raffia
G
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

Gilbertogilbertson answered 1/6, 2016 at 6:51 Comment(8)
My Plunker seems to work fine. How can I reproduce the issue you mention?Until
In your plunker, if you put a binding eg. <h2>Hello {{field.name}}</h2>. It has a lowercase last letter.Gilbertogilbertson
Also, in angular rc1 updating the model triggers the ngModelChange event to trigger again and you get an infinite loop.Gilbertogilbertson
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!Gilbertogilbertson
@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.Regulate
@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!Gilbertogilbertson
@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 occurredDacosta
@Vicky Sorry, Angular has changed so much since here and I suspect this doesn't work anymore.Gilbertogilbertson
A
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

Aalborg answered 19/3, 2016 at 20:12 Comment(7)
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.Chindwin
Sure, just write a comment after you added your answer, so I get notified.Until
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.Until
I tried the combination but that causes endless loops :-/Until
I will experiment a bit more using both options. For now I'm happy to have learned enough to get it right somehow. thnx.Chindwin
#35826825Trickster
Thanks for your help .. nice solution :)Raffia
Y
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>
Yazzie answered 30/12, 2017 at 0:38 Comment(0)
J
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);
}
}
Jedda answered 18/1, 2019 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.