Skip to content

Instantly share code, notes, and snippets.

@junftnt
Forked from gund/mask.md
Created February 15, 2020 00:05
Show Gist options
  • Select an option

  • Save junftnt/e42912aeb4b422bd97f85f6b90263412 to your computer and use it in GitHub Desktop.

Select an option

Save junftnt/e42912aeb4b422bd97f85f6b90263412 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 appMask="000-AAA-0">

Implementation

mask.directive.ts

import { Directive, Injector, Input, OnInit } from '@angular/core';
import { NgControl } from '@angular/forms';

import { valueToFormat, unmaskValue } from './mask';

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

  @Input() appMask: string;

  private control: NgControl;
  private _lastMaskedValue = '';

  constructor(
    private injector: Injector,
  ) { }

  ngOnInit() {
    this.control = this.injector.get(NgControl);

    if (!this.control || !this.control.valueAccessor) {
      return;
    }

    const originalWriteVal = this.control.valueAccessor.writeValue.bind(this.control.valueAccessor);
    this.control.valueAccessor.writeValue = (val: any) => originalWriteVal(this._maskValue(val));

    const originalChange = (<any>this.control.valueAccessor)['onChange'].bind(this.control.valueAccessor);
    this.control.valueAccessor.registerOnChange((val: any) => originalChange(this._unmaskValue(val)));

    this._setVal(this._maskValue(this.control.value));
  }

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

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

    return maskedVal;
  }

  private _unmaskValue(val: string): string {
    const maskedVal = this._maskValue(val);
    const unmaskedVal = unmaskValue(maskedVal);

    if (maskedVal !== val) {
      this._setVal(maskedVal);
    }

    return maskedVal ? unmaskedVal : '';
  }

  private _setVal(val: string) {
    if (this.control.control) {
      this.control.control.setValue(val, { emitEvent: false });
    }
  }

}

mask.ts

import { StringHashMap } from '../../core/facade/types';

const _formatToRegExp: StringHashMap<RegExp> = {
  '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
 */
export 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.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;
}

export function unmaskValue(value: string): string {
  const unmaskedMathes = value.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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment