Masks for inputs on Ionic v4
Ionic Info
Ionic:
ionic (Ionic CLI) : 4.1.1 (/usr/local/lib/node_modules/ionic)
Ionic Framework : @ionic/angular 4.0.0-beta.3
@angular-devkit/core : 0.7.0-rc.3
@angular-devkit/schematics : 0.7.0-rc.3
@angular/cli : 6.0.8
@ionic/ng-toolkit : 1.0.0
@ionic/schematics-angular : 1.0.1
System:
NodeJS : v9.1.0 (/usr/local/bin/node)
npm : 5.5.1
OS : Linux 4.4
Describe the Feature Request
Being able to natively add masks to ion-input, a pretty standard feature for most applications.
Describe Preferred Solution
<ion-input mask="(999) 999-9999" mask-placeholder="_"></ion-input>
+1
+1
Totally necessary to all my apps.
+1 I think this is necessary as ionic 4 still utilizes Angular's control value accessor, preventing the use of other libraries that attempt to do the same (ie angular2-text-mask).
Thanks guys for your response, but is mask really available for input? Can someone send me a link? https://www.w3schools.com/tags/tag_input.asp there is no mask and what I found with mask was only with jquery
I think you guys could develop a property for opening keyboard numeric or text, (even with type="text" or type="password" and also develop a property for binding masks (regex), for example, phone masks:
(323) 232-3233, like this: https://github.com/text-mask/text-mask
The masks with ionic 4 is a big challenge, as @zakton5 said, because ionic 4 utilizes Angular.s control value accessor.
Yeah, would be great if you could add a PR :)
It could easily utilize the text-mask
library as @luishmcmoreno suggested and accept a mask
property just as the angular bindings do.
Here is my solution in the meantime for Reactive forms using text-mask
. The method can also be adapted for Template driven forms.
In text example I only type 8505555555
. If you try and enter letters the text-mask library will remove them as it does not match the mask. When backspacing the text out it automatically removes the parentheses and dashes.
<ion-content padding>
<ion-item>
<ion-label>Phone Number</ion-label>
<ion-input #phoneInput [formControl]="phoneNumber"></ion-input>
</ion-item>
</ion-content>
import { Component, ViewChild } from '@angular/core';
import { IonInput } from '@ionic/angular';
import {
FormBuilder,
FormGroup,
Validators,
FormControl,
} from '@angular/forms';
import { createTextMaskInputElement } from 'text-mask-core';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
@ViewChild('phoneInput')
public set phoneInput(value: IonInput) {
if (!value) {
return;
}
value.getInputElement().then(input => this.registerTextMask(input));
}
public form: FormGroup = this.fb.group({
phoneNumber: [null, [Validators.required]],
});
// prettier-ignore
private phoneMask = ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/];
get phoneNumber() {
return this.form.get('phoneNumber') as FormControl;
}
constructor(private fb: FormBuilder) {}
private registerTextMask(inputElement: HTMLInputElement) {
const maskedInput = createTextMaskInputElement({
inputElement,
mask: this.phoneMask,
guide: false,
});
this.phoneNumber.valueChanges.subscribe(value => {
maskedInput.update(value);
});
}
}
+1 for Ionic implementation asap. My current workaround with Ionic 4 is using a combination of Angular Material and angular2-text-mask. Here's a working example with masks for D.O.B. and UK mobile (with reactive forms):
___module.ts
imports: [
...
MatFormFieldModule,
MatInputModule,
TextMaskModule,
...
],
component.ts
dateMask = [/[0-3]/, /\d/, ' ', '/', ' ', /[0-1]/, /\d/, ' ', '/', ' ', /[1-2]/, /\d/, /\d/, /\d/,];
mobileNumMask = ['0', '7', /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/]
// 'Unmask' the D.O.B.
// and change from dd/mm/yyyy to yyyy-mm-dd (ISO 8601 standard)
const chunked = ...dateOfBirth.split(' / ');
const formattedDate = `${chunked[2]}-${chunked[1]}-${chunked[0]}`;
component.html
<!-- Mobile -->
<ion-item lines="none">
<mat-form-field appearance="legacy">
<mat-label>Mobile Number</mat-label>
<input
type="tel"
matInput
formControlName="mobNumber"
[textMask]="{ mask: mobileNumMask, placeholderChar: '\u2000' }"
/>
</mat-form-field>
</ion-item>
<p class="error-message" *ngIf="!mobNumber.valid && mobNumber.dirty">
Valid Mobile Number is required
</p>
<!-- Date of Birth -->
<ion-item lines="none">
<mat-form-field>
<mat-label>Date of Birth</mat-label>
<input
placeholder="dd/mm/yyyy"
matInput
formControlName="dateOfBirth"
[textMask]="{ mask: dateMask, placeholderChar: '\u2000' }"
/>
</mat-form-field>
</ion-item>
<p class="error-message" *ngIf="!dateOfBirth.valid && dateOfBirth.dirty">
Valid Date of Birth is required
</p>
And to make the material elements ~consistent with ionic:
component.scss
mat-form-field {
width: 100%;
padding-top: 16px;
}
global.scss
.mat-focused .mat-form-field-ripple {
background-color: var(--ion-color-primary !important;
}
.mat-form-field-appearance-legacy .mat-form-field-underline {
background-color: var(--ion-color-light-shade) !important;;
}
.mat-focused .mat-form-field-label {
color: var(--ion-color-dark) !important;;
}
Hey @adamduren thanks for this. We've used this to one of our apps but when build on iOS it does not work? Do you guys have any work around for it?
@jrayga apologies for the delayed response. I was not aware of the iOS bug. Can confirm it exists but am not sure of the cause or workarounds at the moment.
@adamduren Thank you for responding! I'm still finding a way to have credit-card masking on our app. Again thank you for sharing that code. God Bless.
@jrayga I found a simple albeit hacky workaround.
I changed the following in registerTextMask()
:
this.phoneNumber.valueChanges.subscribe(value => {
maskedInput.update(value);
});
to:
this.phoneNumber.valueChanges
.pipe(
distinctUntilChanged(),
// This seemed to do the trick
delay(50),
)
.subscribe(value => {
maskedInput.update(value);
});
It's a short delay that seems to avoid whatever race condition is occurring although because it appears to be a race condition that is not always guaranteed to work. Wish I had a better answer for what's going on under the hood but don't really have time at the moment to do much more digging. Will update if I'm able to get around to it.
Actually I was able to make a directive and it seems to work pretty well on desktop, iOS, and android. No hacks required.
import { Directive, OnDestroy, OnInit } from '@angular/core';
import { IonInput } from '@ionic/angular';
import { createTextMaskInputElement } from 'text-mask-core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
// prettier-ignore
const phoneMask = ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/,];
@Directive({
selector: '[prPhoneMask]',
providers: [IonInput],
})
export class PhoneMaskDirective implements OnInit, OnDestroy {
private onDestroy$ = new Subject<void>();
constructor(public ionInput: IonInput) {}
public ngOnInit() {
this.configurePhoneInput();
}
public ngOnDestroy() {
this.onDestroy$.next();
}
public async configurePhoneInput() {
const input = await this.ionInput.getInputElement();
const maskedInput = createTextMaskInputElement({
inputElement: input,
mask: phoneMask,
});
this.ionInput.ionChange
.pipe(takeUntil(this.onDestroy$))
.subscribe((event: CustomEvent) => {
const { value } = event.detail;
maskedInput.update(value);
this.ionInput.value = input.value;
});
}
}
In case someone has tried the directive above and have found that it does not function quite right on an iOS device, I modified
const maskedInput = createTextMaskInputElement({
inputElement: input,
mask: phoneMask,
});
to
const maskedInput = createTextMaskInputElement({
inputElement: input,
mask: phoneMask,
guide:false
});
and it works perfectly.
I ran into the same race condition issue - debugging the ionic source, it looks like it is because there's a collision between the input events that are issued. The ionic web component gets the 'input' event and triggers a re-render, meanwhile, the text-mask has already set the raw input element's value. This gets stomped on by the ion-input render.
I fixed this by trying to play nice with the text-mask library, and "pretending" to be a real input, rather than an ion-input. note I'm using ionic 4 with vanilla, not angular.
To make this work, I created a Proxy and trapped a couple props. After that, everything works beautifully.
The proxy:
import textMask from 'vanilla-text-mask';
function createMask(options) {
let original_input = options.inputElement;
let proxy = new Proxy(original_input, {
get(target, key) {
switch(key) {
case 'addEventListener':
return original_input.addEventListener.bind(original_input);
case 'selectionEnd':
return original_input['selectionEnd'];
default:
return target[key];
}
},
set(target, key, value) {
target[key] = value;
return true;
}
});
options.inputElement = proxy;
return textMask(options);
}
export default createMask;
To use it:
let input_phone_cell = this.shadowRoot.querySelector('#phone_cell');
let mask = ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]
this.mask_controller = createMask({
inputElement: input_phone_cell,
mask: mask,
guide: false,
placeholderChar: '\u2000'
});
@adamduren Thanks for this solution. It works well for me, however I have noticed that if I enter an extra character at the end it will include this extra character after I click off of the input. (ex. (123) 555-55550)
Based on @adamduren solution, here is a more generic directive :
// Angular
import { Directive, Input } from '@angular/core';
// Ionic
import { IonInput } from '@ionic/angular';
// Rxjs
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
// Text-mask
import { createTextMaskInputElement } from 'text-mask-core';
/**
* ion-mask directive, based on text-mask module
*/
@Directive({
selector: '[ionMask]',
providers: [IonInput],
})
export class IonMask {
@Input('ionMask')
private mask : Array<any> = [];
private onDestroy : Subject<void> = new Subject<void>();
constructor(public ionInput: IonInput) {}
public ngOnInit() {
this.configureInput();
}
public ngOnDestroy() {
this.onDestroy.next();
}
public async configureInput() {
const input = await this.ionInput.getInputElement();
const maskedInput = createTextMaskInputElement({
inputElement : input,
mask : this.mask,
guide : false
});
this.ionInput
.ionChange
.pipe( takeUntil( this.onDestroy ) )
.subscribe( ( event: CustomEvent ) => {
const { value } = event.detail;
maskedInput.update(value);
this.ionInput.value = input.value;
});
}
}
Use it like that in your component's template :
<ion-input formControlName="controlName" [ionMask]="mask"></ion-input>
And in your component's controller :
public mask : Array<any> = [ 'y','o', 'u', 'r', ' ', 'm', 'a', 's', 'k' ]
text-mask-core
thats a great post .could not find better phone masking logic elsewhere.
one more addition to the above generic approah by clmntr.
in case i am loading an existing phone number the masking does not work with the above code.if we include the below code that issue is taken care
public async configureInput() {
const input = await this.ionInput.getInputElement();
const maskedInput = createTextMaskInputElement({
inputElement: input,
mask: this.mask,
guide: false
});
// masking when event is not generated
maskedInput.update(input.value);
this.ionInput.value = input.value;
// masking when event is generated
this.ionInput
.ionChange
.pipe()
.subscribe((event: CustomEvent) => {
const { value } = event.detail;
maskedInput.update(value);
this.ionInput.value = input.value;
});
}
Based on @adamduren solution, here is a more generic directive :
// Angular import { Directive, Input } from '@angular/core'; // Ionic import { IonInput } from '@ionic/angular'; // Rxjs import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; // Text-mask import { createTextMaskInputElement } from 'text-mask-core'; /** * ion-mask directive, based on text-mask module */ @Directive({ selector: '[ionMask]', providers: [IonInput], }) export class IonMask { @Input('ionMask') private mask : Array<any> = []; private onDestroy : Subject<void> = new Subject<void>(); constructor(public ionInput: IonInput) {} public ngOnInit() { this.configureInput(); } public ngOnDestroy() { this.onDestroy.next(); } public async configureInput() { const input = await this.ionInput.getInputElement(); const maskedInput = createTextMaskInputElement({ inputElement : input, mask : this.mask, guide : false }); this.ionInput .ionChange .pipe( takeUntil( this.onDestroy ) ) .subscribe( ( event: CustomEvent ) => { const { value } = event.detail; maskedInput.update(value); this.ionInput.value = input.value; }); } }
Use it like that in your component's template :
<ion-input formControlName="controlName" [ionMask]="mask"></ion-input>
And in your component's controller :
public mask : Array<any> = [ 'y','o', 'u', 'r', ' ', 'm', 'a', 's', 'k' ]
Can you tell me how can I switch between masks?
Ex.:
public mask: Array<any> = [];
public mask1 : Array<any> = [ 'a','a', 'a', 'a', ' ', 'b', 'b', 'b', 'b' ]
public mask2 : Array<any> = [ 'y','o', 'u', 'r', ' ', 'c', 'c', 'c', 'c' ]
this.inputCountryCode.valuesChange.subscribe( val => {
if ( val == '598') { this.mask = this.mask1};
if ( val == '44') { this.mask = this.mask2};
});
Hi @ozzpy,
Did you try something like this? It also include the fix by @divyasanthoshi 👍
warning: not tested, it might need to be adapted, but the overall idea is there 😃
// Angular
import { Directive, Input, OnInit, OnDestroy } from '@angular/core';
// Ionic
import { IonInput } from '@ionic/angular';
// Rxjs
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
// Text-mask
import { createTextMaskInputElement } from 'text-mask-core';
/**
* ion-mask directive, based on text-mask module
*/
@Directive({
selector: '[ionMask]',
providers: [IonInput],
})
export class IonMask implements OnInit, OnDestroy {
@Input('ionMask')
private mask : Array<any> = [];
private onDestroy : Subject<void> = new Subject<void>();
constructor (public ionInput: IonInput) {}
public ngOnInit () {
this.configureInput();
}
public ngOnDestroy () {
this.onDestroy.next();
}
public async configureInput () {
const input = await this.ionInput.getInputElement();
const maskedInput = createTextMaskInputElement();
const textMaskConfig = {
inputElement : input,
mask : this.mask,
guide : false
};
// masking when event is not generated
maskedInput.update(input.value, textMaskConfig);
this.ionInput.value = input.value;
// masking when event is generated
this.ionInput
.ionChange
.pipe( takeUntil( this.onDestroy ) )
.subscribe( ( event: CustomEvent ) => {
const { value } = event.detail;
maskedInput.update(value, {
...textMaskConfig,
mask: this.mask
});
this.ionInput.value = input.value;
});
}
}
If this doesn't work, we can also try using the ngOnChanges lifecycle method to re-configure the mask. Let see how it goes with this one.
did someone know how to implement that with react ?
It's working with
<IonItem>
<MaskedInput
className="masked-input native-input sc-ion-input-md"
mask={['C', 'H', 'F', ' ', /\d/, /\d/, '.', /\d/, /\d/]}
showMask={true}
/>
</IonItem>
But I loose the focus style
@xero88 Did you find a solution for React?
I was able to find a solution with angular
On Sat, Aug 22, 2020, 8:45 PM Ethan Orlander notifications@github.com
wrote:
@xero88 https://github.com/xero88 Did you find a solution for React?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/ionic-team/ionic-framework/issues/15424#issuecomment-678718067,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ALG3YRSYWF256N36QX57GXLSCBYCVANCNFSM4FSTWPZA
.
I was able to find a solution with angular
…
On Sat, Aug 22, 2020, 8:45 PM Ethan Orlander @.*> wrote: @xero88 https://github.com/xero88 Did you find a solution for React? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#15424 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/ALG3YRSYWF256N36QX57GXLSCBYCVANCNFSM4FSTWPZA .
Show it! Share your knowledge for us.
For masking i issued the package text-mask-core,
Here is how i used the package for phone masking on ion-input
import { Directive, Input, OnInit, OnDestroy } from '@angular/core';
import { IonInput } from '@ionic/angular';
import { Subject } from 'rxjs';
import { createTextMaskInputElement } from 'text-mask-core';
import { NgControl } from '@angular/forms';
@Directive({
selector: '[appPhoneMask]',
providers: [IonInput],
})
export class PhoneMaskDirective implements OnInit {
@Input('appPhoneMask')
private mask: Array
private onDestroy: Subject
constructor(public ionInput: IonInput, public ngControl: NgControl) { }
public ngOnInit() {
this.configureInput();
}
// public ngOnDestroy() {
// this.onDestroy.next();
// }
public async configureInput() {
const input = await this.ionInput.getInputElement();
if (input.value !== '0') {
const maskedInput = createTextMaskInputElement({
inputElement: input,
mask: this.mask,
guide: false
});
// masking when event is not generated useful when loading
maskedInput.update(input.value);
this.ionInput.value = input.value;
// masking when event is generated
this.ionInput
.ionChange
.pipe()
.subscribe((event: CustomEvent) => {
const { value } = event.detail;
maskedInput.update(value);
this.ionInput.value = input.value;
});
}
}
}
On Wed, Aug 26, 2020 at 10:15 AM damtaipu notifications@github.com wrote:
I was able to find a solution with angular
… <#m_-5391329821279774264_m_-359066733190038625_>
On Sat, Aug 22, 2020, 8:45 PM Ethan Orlander @.*> wrote: @xero88
https://github.com/xero88 https://github.com/xero88 Did you find a
solution for React? — You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub <#15424 (comment)
https://github.com/ionic-team/ionic-framework/issues/15424#issuecomment-678718067>,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ALG3YRSYWF256N36QX57GXLSCBYCVANCNFSM4FSTWPZA
.Show it! Share your knowledge for us.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/ionic-team/ionic-framework/issues/15424#issuecomment-680944106,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/ALG3YRWC32NJM7K74W2TLIDSCURJFANCNFSM4FSTWPZA
.
Most helpful comment
+1