Skip to content

Instantly share code, notes, and snippets.

@rami-alloush
Forked from gund/mask.md
Last active December 24, 2022 08:12
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save rami-alloush/3ee792fd0647b73de5f863a2719c78c6 to your computer and use it in GitHub Desktop.
Save rami-alloush/3ee792fd0647b73de5f863a2719c78c6 to your computer and use it in GitHub Desktop.
Simple Angular mask directive

Simple Angular mask directive

This directive does not create it's own value accessor - it simply reuses whatever element is using already and just hooks in.

Also it is fully abstracted off of the HTML implementation and so can be safely used in WebWorker and server side environment.

Usage

<input type="text" name="masked-value" ngModel ngxMask="000-AAA-0">

Make sure you add it to the module declarations

declarations: [MaskDirective],

If you want to use it in mutiple modules, it's better to create a separate module for it

custom-directives.module.ts

import { NgModule } from '@angular/core';
import { MaskDirective } from './mask.directive';

@NgModule({
    declarations: [MaskDirective],
    exports: [MaskDirective],
})
export class CustomDirectives { }

Implementation

mask.directive.ts

import { Directive, Input, OnInit, ElementRef, HostListener } from '@angular/core';


@Directive({
    selector: '[ngxMask]',
})
export class MaskDirective implements OnInit {

    @Input('ngxMask') ngxMask: string;

    private inputElem: HTMLInputElement;
    private _lastMaskedValue = '';

    constructor(
        private el: ElementRef,
    ) { }

    ngOnInit() {
        this.inputElem = this.el.nativeElement;
    }

    @HostListener('input')
    onInput() {
        this.inputElem.value = this._maskValue(this.inputElem.value);
    }

    private _maskValue(val: string): string {
        if (!val || !this.ngxMask || val === this._lastMaskedValue) {
            return val;
        }

        const maskedVal = this._lastMaskedValue =
            valueToFormat(
                val,
                this.ngxMask,
                this._lastMaskedValue.length > val.length,
                this._lastMaskedValue);

        return maskedVal;
    }

}

const _formatToRegExp = {
    '0': /[0-9]/, 'a': /[a-z]/, 'A': /[A-Z]/, 'B': /[a-zA-Z]/,
};

const _allFormatsStr = '(' +
    Object.keys(_formatToRegExp)
        .map(key => _formatToRegExp[key].toString())
        .map(regexStr => regexStr.substr(1, regexStr.length - 2))
        .join('|')
    + ')';

const _allFormatsGlobal = getAllFormatRegexp('g');

/**
 * Apply format to a value string
 *
 * Format can be constructed from next symbols:
 *  - '0': /[0-9]/,
 *  - 'a': /[a-z]/,
 *  - 'A': /[A-Z]/,
 *  - 'B': /[a-zA-Z]/
 *
 * Example: 'AAA-00BB-aaaa'
 * will accept 'COD-12Rt-efww'
 *
 * @param value Current value
 * @param format Format
 * @param goingBack Indicates if change was done by BackSpace
 * @param prevValue Pass to precisely detect formatter chars
 */
function valueToFormat(value: string, format: string, goingBack = false, prevValue?: string): string {

    let maskedValue = '';
    const unmaskedValue = unmaskValue(value);

    const isLastCharFormatter = !getAllFormatRegexp().test(value[value.length - 1]);
    const isPrevLastCharFormatter = prevValue && !getAllFormatRegexp().test(prevValue[prevValue.length - 1]);

    let formatOffset = 0;
    for (let i = 0, maxI = Math.min(unmaskedValue.length, format.length); i < maxI; ++i) {
        const valueChar = unmaskedValue[i];
        let formatChar = format[formatOffset + i];
        let formatRegex = getFormatRegexp(formatChar);

        if (formatChar && !formatRegex) {
            maskedValue += formatChar;
            formatChar = format[++formatOffset + i];
            formatRegex = getFormatRegexp(formatChar);
        }

        if (valueChar && formatRegex) {
            if (formatRegex && formatRegex.test(valueChar)) {
                maskedValue += valueChar;
            } else {
                break;
            }
        }

        const nextFormatChar = format[formatOffset + i + 1];
        const nextFormatRegex = getFormatRegexp(nextFormatChar);
        const isLastIteration = i === maxI - 1;

        if (isLastIteration && nextFormatChar && !nextFormatRegex) {
            if (!isLastCharFormatter && goingBack) {
                if (prevValue && !isPrevLastCharFormatter) {
                    continue;
                }
                maskedValue = maskedValue.substr(0, formatOffset + i);
            } else {
                maskedValue += nextFormatChar;
            }
        }
    }

    return maskedValue;
}

function unmaskValue(value: string): string {
    const unmaskedMathes = value.replace(' ', '').match(_allFormatsGlobal);
    return unmaskedMathes ? unmaskedMathes.join('') : '';
}

function getAllFormatRegexp(flags?: string) {
    return new RegExp(_allFormatsStr, flags);
}

function getFormatRegexp(formatChar: string): RegExp | null {
    return formatChar && _formatToRegExp[formatChar] ? _formatToRegExp[formatChar] : null;
}

Limitations

For now, you can't use to consecutive formatting characters. For example, this mask will not work ngxMask="(000)-AAA-0"

@WiktorNowikow
Copy link

WiktorNowikow commented Jun 16, 2020

Greetings! Very cool! Can you tell me please? is it possible to full '_', for all empty space when i am typing in input?
Best
Viktor

@rami-alloush
Copy link
Author

Hi @WiktorNowikow, you mean in the mask itself? please explain with an example. Thanks

@WiktorNowikow
Copy link

Hi @WiktorNowikow, you mean in the mask itself? please explain with an example. Thanks

Yes! i mean, when i am typing i want to see something like this in my text input: 33_______
Best

@rami-alloush
Copy link
Author

I'll try this and come back to you (hopefully soon) thanks :)

@lucianosantana
Copy link

lucianosantana commented Jul 15, 2020

Thank you for sharing. I've just made some improvements on the code to make it simple and to overcome the described limitation.

  /**
   * Apply format to a value string
   *
   * Format can be constructed from next symbols:
   *  - '0': /[0-9]/,
   *  - 'a': /[a-z]/,
   *  - 'A': /[A-Z]/,
   *  - 'B': /[a-zA-Z]/
   *
   * Example: 'AAA-00BB-aaaa'
   * will accept 'COD-12Rt-efww'
   *
   * @param value Current value
   * @param format Format
   */
  private valueToFormat(value: string, format: string): string {
    const unmaskedValue = this.unmaskValue(value);
    const maskedValueArray = unmaskedValue.split('');
    for (let formatCharPosition = 0;  formatCharPosition < format.length; formatCharPosition++) {
      const valueChar = maskedValueArray[formatCharPosition];
      // Do skip position if no value was inputted at this position
      if (valueChar === undefined) {
        continue;
      }

      let formatChar: string = format[formatCharPosition];
      let formatRegex = this.getFormatRegexp(formatChar);

      const isSeparator = formatChar && !formatRegex;
      if (isSeparator) {
        // Adds separator on correct position and skips formatting
        maskedValueArray.splice(formatCharPosition, 0, formatChar);
        continue;
      }

      if (valueChar && formatRegex && formatRegex.test(valueChar)) {
          // Adds formatted char to the correct position
          maskedValueArray.splice(formatCharPosition, 1, valueChar);
      }

    }

    // Join all parsed value, limiting length to the one specified in format
    return maskedValueArray.join('').substr(0, format.length);
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment