ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
50.92k stars 13.51k forks source link

Mask for ion-input #15424

Closed Leonavas closed 1 year ago

Leonavas commented 6 years ago

Feature Request

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>
mateusduraes commented 6 years ago

+1

luishmcmoreno commented 6 years ago

+1 Totally necessary to all my apps.

zakton5 commented 5 years ago

+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).

paulstelzer commented 5 years ago

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

luishmcmoreno commented 5 years ago

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.

paulstelzer commented 5 years ago

Yeah, would be great if you could add a PR :)

adamduren commented 5 years ago

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);
    });
  }
}

phonemask

jaipresto commented 5 years ago

+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;;
}
jrayga commented 5 years ago

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?

adamduren commented 5 years ago

@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.

jrayga commented 5 years ago

@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.

adamduren commented 5 years ago

@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);
  });
adamduren commented 5 years ago

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.

adamduren commented 5 years ago

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;
      });
  }
}
danieldugger commented 5 years ago

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.

claytongulick commented 5 years ago

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'
        });
brenden-t-r commented 5 years ago

@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)

clmntr commented 4 years ago

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' ]
divyasanthoshi commented 4 years ago
text-mask-core

thats a great post .could not find better phone masking logic elsewhere.

divyasanthoshi commented 4 years ago

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;
  });

}

ozzpy commented 4 years ago

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}; });

clmntr commented 4 years ago

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.

anthony-bernardo commented 4 years ago

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

Normal behavior (with blue line) :

Capture d’écran de 2020-08-07 01-53-19

Wrong behavior :

Capture d’écran de 2020-08-07 01-53-00

EthanOrlander commented 4 years ago

@xero88 Did you find a solution for React?

divyasanthoshi commented 4 years ago

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 .

damtaipu commented 4 years ago

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.

divyasanthoshi commented 4 years ago

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 = new 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_-5391329821279774264m-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 .

SalahAdDin commented 3 years ago

What's about for React?

jctiradopatientnow commented 3 years ago

Are you considering to add the mask enhancement in ionic/react?

EthanOrlander commented 3 years ago

It's easy enough to create your own HOC around IonInput for simple, specific masks, but when it comes to integrating with something like iMask, it gets more complicated.

I know Ionic's @ehorodyski-ionic has played around with masks in Ionic React's IonInput component. I'm tracking this issue he created on iMask to see if anything comes of it.

EthanOrlander commented 3 years ago

I can see two demos on Eric's Github, if these are helpful for any of y'all. These are super not official! But maybe they can get the ball rolling for your own implementations.

https://github.com/ehorodyski-ionic/fw-react-input-masking https://github.com/ehorodyski-ionic/poc-fw-react-input-masking

DavidTheProgrammer commented 3 years ago

For anybody looking to use this for a number mask. It's possible to use the createNumberMask method from text-mask-addons and pass it as the input. This works even when you import the types because the return type of the createNumberMask function is any. Here's an example:

In your *.ts file

amountMask = createNumberMask({ prefix: '', allowDecimal: true });

In your *.html

<ion-input formControlName="amount" [ionMask]="amountMask"></ion-input>
EinfachHans commented 1 year ago

The best (angular) library i know for this is ng-mask, which sadly doesn't work well with ionic. Would be great to adapt as many features as possible from there into the ionic input & textarea. For example prefix and suffix are an awesome feature as well!

lhenri commented 1 year ago

If it helps, here is the best solution I found for the moment : I'm using the ngx-mask (https://www.npmjs.com/package/ngx-mask) package but I can't load its native directive directly into my HTML templates. So I write one that calls the NgxMaskPipe, also provided with ngx-mask to transform my ion-input values.

my-mask.directive.ts --- import {Directive, HostListener, Input} from '@angular/core'; import {NgxMaskPipe} from 'ngx-mask'; import {NgControl} from '@angular/forms'; @Directive({ selector: 'ion-input[myMask]' }) export class MyMaskDirective { @Input() myMask: string;

constructor(private maskPipe: NgxMaskPipe, private control: NgControl) { }

@HostListener('ionChange') onChange() { this.control.valueAccessor.writeValue(this.maskPipe.transform(this.control.value, this.myMask)); } } ---

And in my template, I can now use : <ion-input (ionChange)="suggest()" formControlName="zipCode" inputmode="numeric" myMask="00 000" name="zipCode" placeholder="00 000" type="text"></ion-input>

I'm using this solution since Ionic 5. So I can confirm it works for me with :

Ionic 5 / Angular 14 / ngx-mask 14

Ionic 6 / Angular 14 or 15 / ngx-mask 14 or 15.

liamdebeasi commented 1 year ago

Hi everyone,

The team is very excited to announce that support for this is coming to Ionic! Developers will be able to leverage Maskito with components such as ion-input.

The work required to support this has been completed, and we plan to announce this feature in the next minor release of Ionic soon.

SalahAdDin commented 1 year ago

Hi everyone,

The team is very excited to announce that support for this is coming to Ionic! Developers will be able to leverage Maskito with components such as ion-input.

The work required to support this has been completed, and we plan to announce this feature in the next minor release of Ionic soon.

Wow, that library is very interesting!

ionitron-bot[bot] commented 1 year ago

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.