hey guys!
I'm having problem using a custom value accessor with ion-input.
what I'm trying to do is to format an input text field while the user is typing, to enhance the user experience, like this: 12345678900a => 123.456.789-00 (it removes all the non-digit characters and apply the dots and dash).
to achieve this, I've created a custom value accessor as discussed in https://github.com/angular/angular/issues/6174.
the problem is that when I use the custom directive with a "pure" input, the value saved to ngFormControl is correct, but when I use the same directive in a ion-input, the value passed to the ngFormControl is not the one that the user is actually seeing, but the value before the formatting (for instance, if the user types 123a, what he sees in the input is 123, but the value in the model is 123a).
thank you and congrats for the outstanding work with Ionic 2!
the code (excerpts) from the custom value accessor, form component and form template:
custom value accessor
@Directive ({
selector: '[agl-id-num]',
host: {
'(input)': 'doOnChange($event.target)',
'(blur)': 'doOnBlur($event.target)'
},
bindings: [ CUSTOM_VALUE_ACCESSOR ]
})
export class IdNumAccessor extends DefaultValueAccessor {
constructor(_renderer: Renderer, _elementRef: ElementRef) {
super(_renderer, _elementRef);
}
writeValue(value:any):void {
if (value!=null) {
super.writeValue(value);
}
}
doOnChange(elt) {
// prevent user to input non-digit characters while typing
// and limit user input to 11 characters
let val = elt.value
.replace(/\D/g, '')
.substring(0,11);
elt.value = val; // this is what changes the input value as user types
this.onChange(val); // this is what saves the value to the model
}
doOnBlur(elt) {
// format field on blur only
let val = elt.value;
if (val.length === 11) {
elt.value = val.replace(/^(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
}
}
}
form component
@Component({
selector: 'agl-client-register-form',
templateUrl: 'build/components/clients/client-register-form/client-register-form.component.html',
directives: [ FORM_DIRECTIVES, IdNumAccessor ]
})
export class ClientRegisterFormComponent implements OnInit {
form: ControlGroup;
@Output() onFormSubmit = new EventEmitter();
@Input() initialValues;
constructor(
private fb: FormBuilder) {}
ngOnInit() {
console.log(this.initialValues);
this.form = this.fb.group({
'name': [this.initialValues.name, Validators.required],
'idNum': [this.initialValues.idNum, Validators.required],
'idNumTest': [this.initialValues.idNum, Validators.required],
'email': [this.initialValues.email],
'celphone': [this.initialValues.celphone],
'sendBoletoToMyself': [this.initialValues.sendBoletoToMyself]
});
}
onSubmit():void {
console.log(this.form.value);
}
}
form template
<form novalidate
[ngFormModel]="form"
(ngSubmit)="onSubmit()">
<ion-list>
<!-- using with "pure" input -->
<input
agl-id-num
type="tel"
[ngFormControl]="form.controls['idNumTest']"
required />
<ion-item>
<ion-label floating>Id Num*</ion-label>
<!-- using with ion-input (described problem) -->
<ion-input
agl-id-num
type="tel"
[ngFormControl]="form.controls['idNum']"
required>
</ion-input>
</ion-item>
</ion-list>
<div padding>
<button block type="submit">Register</button>
</div>
</form>
using Ionic 2.0.0-beta.6
Hello! Thanks for opening an issue with us! Would you mind making a plunker that demonstrates this issue please?
sure! there it is: http://embed.plnkr.co/b2rvq3uHcrxpmzTZ23f1/
I've applied the custom num accessor to both inputs (the ion one and the "pure" one).
note that in the ion-input, when I type a non digit character it is reflected in the form.value, but in the non-ion input it doesn't happen.
This might be related to https://github.com/angular/angular/issues/5328. Since upgrading to Ionic 2.0.0-beta.7 I'm struggling with the issue that Ionic and Angular form handling seems to be constantly out of sync by one event.
Edit: My issue only occurs when using ChangeDetectionStrategy.OnPush though.
@robertdp hum... it could be somehow related. thanks for referencing the issue!
in my case, what seems to be happening is that the DefaultValueAccessor is conflicting with my IdNumAccessor in the ion-input: the onChange event exists in both accessors, but only when used in an ion-input (with html input this doesn't happen, as shown in the plunker) it looks like the onChange in the IdNumAccessor is not being triggered and therefore the related model is binded to the value _before_ the element.value change that happens in IdNumAccessor.
Yeah, the similarities were superficial. The deeper I went into the issue with change detection the less it seemed to apply to this issue.
any news on this issue?
(ps: upgrading to the current beta 8 does not help on this...)
This is still a problem in 2.0.0-beta.10
Hi @endoooo,
any news on this issue? I have the same problem with ionic 2 RC-0
Regards
hi @jbgomez21!
i think it's still an issue... unfortunately.
in the end we used HTML inputs and styled them like ion-inputs. it's not the perfect solution and it was not what we really wanted (all the MD animations were gone), but it worked.
Hi @endoooo
It is not an ionic 2 problem
I have created a plunker example about this problem.
http://embed.plnkr.co/lWpjyCwL0l2r06u1BI0a/ (Running on Chrome)
with 16 digits work fine, but with 17 digits the input label show 1 extra character!
I think that I have a conceptual problem about what happened. Can someone explain it to me please?
@endoooo @jbgomez21 You can pass the FormControl inside the directive and call setValue manually. This way the FromGroup will be updated
@jbgomez21 I've made some changes in your plunker, and now it's working as it should: https://embed.plnkr.co/0vfCj6AyjfQgLGxDtDpj/
there were some problems with your implementation:
@Directive) was mixed with the input @Component. roughly speaking, the value accessor cares about which value should be presented in the component and how it should be saved in its model, and the input component cares about the user interface.@Input values to the child components were a bit messy (no []s).there were some other changes I've made. check the code and see if you can understand them.
@NelsonBrandao thanks for the tip. I didn't test it (so it may work), but I don't think passing FormControl inside the value accessor is a clean solution. the point of using the value accessor's onChange is exactly to reflect the value to the control model without referencing the FormControl directly, isn't it?
@endoooo Thank you very much for your corrections.
They have been very helpful for me and surely be for other people.
Regards!
@endooo Yup, not the best solution, just a quick fix until there is better one that works on ion-input
Hi @endoooo
This Directive work for me with javascript<ion-input>.
Please, look at and give me your comment
import { Directive, ElementRef, Renderer, OnInit } from '@angular/core';
import { DefaultValueAccessor, NgControl } from '@angular/forms';
declare var VMasker: any;
const CardMaskLength: number = 16;
const Mask: string = '9999-9999-9999-9999';
@Directive({
selector: '[card-mask][ngModel]', // Attribute selector
providers: [DefaultValueAccessor]
})
export class CardMask implements OnInit {
constructor(
private renderer: Renderer,
private elementRef: ElementRef, private model: NgControl) { }
writeValue(value: any): void {
// Write to view
if (value !== null && value !== undefined) {
value = VMasker.toPattern(value, Mask);
this.model.valueAccessor.writeValue(value);
this.renderer.setElementProperty(this.elementRef.nativeElement.firstChild, 'value', value);
}
}
valueChange(value) {
// prevent user to input non-digit characters while typing
// and limit user input to CardMaskLength characters
let val = value.replace(/\D/g, '');
val = val.substring(0, CardMaskLength);
//write formatted to control view
this.writeValue(val);
this.model.viewToModelUpdate(val);
}
ngOnInit() {
this.model.valueChanges.subscribe((value : any) => {
this.valueChange(value);
});
}
}
then, I can use it
<ion-input type="text" placeholder="1234 5678 9012 3456" card-mask [(ngModel)]="_model.cardNumber"></ion-input>
<p>{{_model.cardNumber}}</p>
I do not know why I have to put both statements, but it works.
If I remove one does not work properly.
this.model.valueAccessor.writeValue(value);
this.renderer.setElementProperty(this.elementRef.nativeElement.firstChild, 'value', value);
Can someone explain it to me please?
sorry for my bad english :-)
@jbgomez21 maybe I'm wrong (because I don't have a deep knowledge about what ng2 is doing behind the scenes), but I think that what you're doing here is imitating (in a simpler way) the implementation of the custom value accessor.
it should work for what you need, but I don't know for sure what are the side effects of this implementation. I have three main points of concern with your directive:
this.valueChange(value) and the this.model.viewToModelUpdate(val), for instance. my concern here is that if Angular use those events to trigger another ones, it will start to happen twicethis.model.valueAccessor.writeValue(value) may be triggering the DefaultValueAccessor writeValue method, which is:writeValue(value: any): void {
var normalizedValue = isBlank(value) ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
this doesn't feel right, right? 馃槃
now, in order to figure out why this only works when you use both writeValue and setElementProperty, I'd need to run some tests, but I don't have the time for doing it now... 馃樁
what do you think?
if anyone else could express your thoughts about this topic, it would be great!
@endoooo
Yes, It works with FormControl and FormGroup validators.
I am agree that it isn't the best solution, but my intent was someone explain to me why this works :-)
I don't know why with ion-input doesn't work @Directive that implements CustomValueAccessor. :-(
Regards!
@endoooo and @jbgomez21, thanks for your effort, but how can I solve this Can't bind to 'mask' since it isn't a known property of 'ion-input'. :sob:
<ion-input [mask] placeholder="Manu谩lne zadanie k贸du"
formControlName="serialNumber">
</ion-input>
Hi @luckylooke
Please give me more detail about what are you doing.
If you have a "mask" @Directive then sintaxis is <ion-input mask placeholder="..."> </ion-input>
[mask] refer to input property that ion-input doesn't have
Regards.
mask directive is copy of your card-mask and I wanted to be input so I can pass mask as parameter like so: <ion-input [mask]="[0-9]{6}-[0-9]{6}" placeholder="..."> </ion-input> but it is true that mask attribute directive without [] is accepted by ionic input. Thanks for hint, maybe this will help me go forward 馃
Hi @luckylooke
I attached card-input.zip files, It's a fragment of how I implemented the mask for card input.
I finally wrapped ion-input tag.
I used it that way:
<card-input [card-brand]="card.id" [card-length]="card.length" formControlName="cardNumber" placeholder="1234 5678 9012 3456"
[(ngModel)]="card.cardNumber" (ngModelChange)="onChangeCardNumber($event)" \>
</card-input>
good lucky!
Thank you @jbgomez21, I will try it in project and let you know then.
TLDR: Works in angular, but Ionic structures ion-input components differently so use a service(provider in Ionic).
TLDR2: It might be possible, but in an HTML DOM Input Text Object(<input>)(https://www.w3schools.com/jsref/dom_obj_text.asp), the value of the input is stored within an property of the input object. We don't have to access An ion-input tag, while expressed as an HTML tag, represents a _component_, and the value of the input is now stored in the component, and not within a node on the DOM. While easily implemented in angular as a directive, The ionic framework unintentionally(I assume) makes this type of behavior very difficult/maybe not possible for a directive, instead you should use a service to manipulate the data.
FULL:
I have been working on a problem that contains a very similar issue, the only difference is that I am formatting a phone number. My current solution works for an HTML input tag but it does not work for the ion-input tag, very similar to the issue in this thread. I don't have a solution yet, but here are some considerations after digging around in angular's and ionic's source code and lots of debugging. Also I am wanting to implement this as a custom attribute directive.
Firstly: I am using functions from the Renderer2 Angular library, specifically listen and setProperty. I use the listen():
renderer2.listen(this.nativeElement, 'input', (event => { //process the input string which is accessed by event.target.value }
and then after formatting setProperty():
this.renderer2.setProperty(this.nativeElement, 'value', returnString)
Why this works for an HTML input tag but not an ion-input tag: Initially I thought I had found a solution and was pretty happy with myself, turns out it didn't fix this issue but helped me learn a lot.
copyInputAttributes(ionInputEle, nativeInputEle);const SKIP_INPUT_ATTR = ['value', 'checked', 'disabled', 'readonly', 'placeholder', 'type', 'class', 'style', 'id', 'autofocus', 'autocomplete', 'autocorrect'];OKAY. So I see that 'value' is the first item in the list to be excluded. Well shoot, I'm trying to use Renderer2 to set the 'value' attribute of the 'nativeElement'. If the value attribute doesn't exist on ion-input then clearly this won't work, Ionic excludes the value attribute when constructing an ion-input, and in my directive, I'm trying to set the value of a property that doesn't exist.
At this point I thought I had mastered the world. I went into the ionic-angular module of a test-project and found dom.ts and removed 'value' from SKIP_INPUT_ATTR.
it did not work, I was freakin pissed, f* computers I want to go back to working outside again.
A few hours later I added some console.log's and looked at it again. I inspected the ion-input, and this time there was a value property! And when I typed in a phone number, while it wasn't being formatted in the view, upon inspection the value property was showing a formatted input(such as (888) 888-888 ). So what I was trying to do actually did work, but what I was trying to do wasn't going to fix the problem.
Why this didn't work from the beginning should have been obvious to me. If the 'value' attribute is on the list of 'SKIP_INPUT_ATTR', then no part of ionic-angular will be looking for a value at the 'value' property that normally doesn't exist!
So at this point I've learned that ion-inputs don't use the html value property to store the value of the input. This should have been obvious to me. ion-input's are COMPONENTS!!! I've always though of them as just html tags. The value that the component represent is probably much better defined inside a component instead of an attribute on a HTML node.
Okay so now I need to figure out how and where ion-input's store the value of their input, presumably the variable name will be something like 'value'
@Output() input = new EventEmitter<UIEvent>();STOP.
I went and read the Angular documentation on the directive decorator and have come to some realizations.
An Attribute directive changes the appearance or behavior of a DOM element.
The formatting that we are trying to achieve here changes the actual value(in my case it does). So as much as I want to develop this as an attribute directive that you can easily insert into the template, I think angular is telling us that this would better be implemented in a service.
I'm not sure how clear my above post was so here are two points to take away.
Within an ion-input, the value of the input is stored in the component. This is a different structure than that of an HTML input tag. So when tying to use this directive in an ion-input, we now have to update the value INSIDE the component.
From Angulars page on Attribute Directives.
An Attribute directive changes the appearance or behavior of a DOM element.
This makes me think that the behaviour we want to see, be it formatting a phone number or something else, would better be implemented as a service, since we are altering the actual value of the component.
Please feel free to tell me I'm wrong, I would like to be able to use this as a simple attribute directive within my Ionic projects.
Any update on this? The suggested workarounds aren't particularly neat.
I've created a directive that works with normal inputs as well as ion-inputs, if anyone needs it: https://stackoverflow.com/a/46981629/4850646
Thanks for the issue! We have moved the source code and issues for Ionic 3 into a separate repository. I am moving this issue to the repository for Ionic 3. Please track this issue over there.
Thank you for using Ionic!
Thanks for the issue! We have moved the source code and issues for Ionic 3 into a separate repository. I am moving this issue to the repository for Ionic 3. Please track this issue over there.
Thank you for using Ionic!
Issue moved to: https://github.com/ionic-team/ionic-v3/issues/90