Skip to content

Instantly share code, notes, and snippets.

@dyazincahya
Last active June 25, 2025 14:43
Show Gist options
  • Save dyazincahya/3192b48fd0c13f22ec50c513913e2b97 to your computer and use it in GitHub Desktop.
Save dyazincahya/3192b48fd0c13f22ec50c513913e2b97 to your computer and use it in GitHub Desktop.
Angular Directive for masking input on HTML input elements. Support (Time, Date, Phone, Credit Card or General)
/**
* Angular Directive for masking input on HTML input elements.
*
* Supports several mask types:
* - 'time' : Time format (HH:MM)
* - 'date' : Date format (DD/MM/YYYY)
* - 'phone' : Indonesian phone number format (08XX-XXXX-XXXX)
* - 'credit-card' : Credit card format (XXXX-XXXX-XXXX-XXXX)
* - 'general' : General/custom masking according to the pattern provided in `maskPattern`
*
* Can be used with Angular Reactive Forms.
*
* @example
* <input type="text" formControlName="myTime" xyzMaskInput="time" placeholder="23:59">
* <input type="text" formControlName="myDate" xyzMaskInput="date" placeholder="DD/MM/YYYY">
* <input type="text" formControlName="myPhone" xyzMaskInput="phone" placeholder="0812-3456-7890">
* <input type="text" formControlName="myCard" xyzMaskInput="credit-card" placeholder="0000-0000-0000-0000">
* <input type="text" formControlName="myGeneral" xyzMaskInput="general" placeholder="General Mask">
* <input type="text" formControlName="myNpwp" xyzMaskInput="general" maskPattern="**.***.***.*-***.***" placeholder="XX.XXX.XXX.X-XXX.XXX">
*
* @selector [xyzMaskInput]
* @export
* @class XyzMaskInputDirective
* @implements {ControlValueAccessor}
*
* @Input {MaskType} xyzMaskInput - The mask type to use (time, date, phone, credit-card, general)
* @Input {string} maskPattern - Custom mask pattern for 'general' type (default: '****-****-****-****')
* @Input {boolean} emitUnmaskedValue - If true, submit the raw (unmasked) value to the form control (default: false)
*/
import {
Directive,
ElementRef,
HostListener,
Input,
forwardRef,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
// Definisikan tipe mask yang didukung untuk type safety
export type MaskType = 'time' | 'date' | 'phone' | 'credit-card' | 'general';
@Directive({
selector: '[xyzMaskInput]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => XyzMaskInputDirective),
multi: true,
},
],
})
export class XyzMaskInputDirective implements ControlValueAccessor, OnChanges {
// Input untuk menerima tipe mask. Contoh: 'date', 'phone', dll.
@Input('xyzMaskInput') maskType: MaskType = 'general';
// Input untuk mengontrol nilai yang di-submit.
// false (default): submit nilai ter-masking (mis: "25/06/2025")
// true: submit nilai mentah/unmasked (mis: "25062025")
@Input() emitUnmaskedValue: boolean = false;
// Input untuk pola masking umum/general (jika maskType='general')
// Karakter '*' akan digantikan oleh input pengguna.
@Input() maskPattern: string = '****-****-****-****';
// Fungsi internal untuk berkomunikasi dengan Angular Forms
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
constructor(private el: ElementRef<HTMLInputElement>) {}
// --- Siklus Hidup & ControlValueAccessor ---
ngOnChanges(changes: SimpleChanges): void {
// Jika tipe mask atau pola berubah, format ulang nilai yang ada
if (
changes['maskType'] ||
changes['maskPattern'] ||
changes['emitUnmaskedValue']
) {
const currentValue = this.el.nativeElement.value;
const unmaskedValue = this.getUnmaskedValue(currentValue);
this.updateValue(unmaskedValue);
}
}
writeValue(value: string): void {
// Fungsi ini dipanggil saat form control di-set nilainya secara programatik.
// Ia menerima nilai dari model (bisa mentah atau terformat)
// dan menampilkannya di input sebagai nilai terformat.
const unmaskedValue = this.getUnmaskedValue(value || '');
const formattedValue = this.applyMask(unmaskedValue);
this.el.nativeElement.value = formattedValue;
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.el.nativeElement.disabled = isDisabled;
}
// --- Event Listeners ---
@HostListener('input', ['$event.target.value'])
onInput(value: string): void {
const unmaskedValue = this.getUnmaskedValue(value);
this.updateValue(unmaskedValue);
}
@HostListener('blur')
onBlur(): void {
this.onTouched();
// Validasi akhir saat pengguna meninggalkan input field
const unmaskedValue = this.getUnmaskedValue(this.el.nativeElement.value);
this.finalizeTimeValue(unmaskedValue);
}
// --- Logika Inti ---
private updateValue(unmaskedValue: string): void {
// 1. Format nilai untuk ditampilkan di UI (View Value)
const formattedValue = this.applyMask(unmaskedValue);
this.el.nativeElement.value = formattedValue;
// 2. Tentukan nilai mana yang akan disimpan di form control (Model Value)
if (this.emitUnmaskedValue) {
this.onChange(unmaskedValue);
} else {
this.onChange(formattedValue);
}
}
private getUnmaskedValue(value: string): string {
// Menghapus semua karakter yang bukan digit
return value.replace(/\D/g, '');
}
private applyMask(digits: string): string {
if (!digits) {
return '';
}
// Router untuk memilih fungsi mask yang sesuai
switch (this.maskType) {
case 'time':
return this.applyTimeMask(digits);
case 'date':
return this.applyDateMask(digits);
case 'phone':
return this.applyPhoneMask(digits);
case 'credit-card':
return this.applyCreditCardMask(digits);
case 'general':
return this.applyGeneralMask(digits);
default:
return digits;
}
}
// --- Fungsi Helper untuk Setiap Tipe Mask ---
private applyTimeMask(digits: string): string {
// Format: HH:MM
const value = digits.substring(0, 4);
if (value.length > 2) {
return `${value.substring(0, 2)}:${value.substring(2)}`;
}
return value;
}
private applyDateMask(digits: string): string {
// Format: DD/MM/YYYY
const value = digits.substring(0, 8);
if (value.length > 4) {
return `${value.substring(0, 2)}/${value.substring(2, 4)}/${value.substring(4)}`;
} else if (value.length > 2) {
return `${value.substring(0, 2)}/${value.substring(2)}`;
}
return value;
}
private applyPhoneMask(digits: string): string {
// Format: 08XX-XXXX-XXXX
const value = digits.substring(0, 13);
if (value.length > 8) {
return `${value.substring(0, 4)}-${value.substring(4, 8)}-${value.substring(8)}`;
} else if (value.length > 4) {
return `${value.substring(0, 4)}-${value.substring(4)}`;
}
return value;
}
private applyCreditCardMask(digits: string): string {
// Format: XXXX-XXXX-XXXX-XXXX
return (
digits
.substring(0, 16)
.match(/.{1,4}/g)
?.join('-') || digits
);
}
private applyGeneralMask(digits: string): string {
let maskedValue = '';
let digitIndex = 0;
for (
let i = 0;
i < this.maskPattern.length && digitIndex < digits.length;
i++
) {
if (this.maskPattern[i] === '*') {
maskedValue += digits[digitIndex];
digitIndex++;
} else {
maskedValue += this.maskPattern[i];
}
}
return maskedValue;
}
// --- Fungsi Validasi & Utilitas ---
private finalizeTimeValue(unmaskedValue: string): void {
if (this.maskType !== 'time' || unmaskedValue.length === 0) {
return;
}
let hours = parseInt(unmaskedValue.substring(0, 2), 10);
let minutes = parseInt(unmaskedValue.substring(2, 4), 10);
if (isNaN(hours)) hours = 0;
if (isNaN(minutes)) minutes = 0;
if (hours > 23) hours = 23;
if (minutes > 59) minutes = 59;
const finalUnmasked = `${this.padZero(hours)}${this.padZero(minutes)}`;
this.updateValue(finalUnmasked);
}
private padZero(num: number): string {
return num < 10 ? `0${num}` : `${num}`;
}
}
@dyazincahya
Copy link
Author

dyazincahya commented Jun 25, 2025

XyzMaskInput Directive

Directive for masking input on HTML input elements.

Supports several mask types:

  • time : Time format (HH:MM)
  • date : Date format (DD/MM/YYYY)
  • phone : Indonesian phone number format (08XX-XXXX-XXXX)
  • credit-card : Credit card format (XXXX-XXXX-XXXX-XXXX)
  • general : General/custom masking according to the pattern provided in maskPattern

How to Usage?

Can be used with Angular Reactive Forms.

<input type="text" formControlName="myTime" xyzMaskInput="time" placeholder="23:59">
<input type="text" formControlName="myDate" xyzMaskInput="date" placeholder="DD/MM/YYYY">
<input type="text" formControlName="myPhone" xyzMaskInput="phone" placeholder="0812-3456-7890">
<input type="text" formControlName="myCard" xyzMaskInput="credit-card" placeholder="0000-0000-0000-0000">
<input type="text" formControlName="myGeneral" xyzMaskInput="general" placeholder="General Mask">
<input type="text" formControlName="myNpwp" xyzMaskInput="general" maskPattern="**.***.***.*-***.***" placeholder="XX.XXX.XXX.X-XXX.XXX">

More info

@selector [xyzMaskInput]
@export
@Class XyzMaskInputDirective
@implements {ControlValueAccessor}

@input {MaskType} xyzMaskInput - The mask type to use (time, date, phone, credit-card, general)
@input {string} maskPattern - Custom mask pattern for 'general' type (default: '---')
@input {boolean} emitUnmaskedValue - If true, submit the raw (unmasked) value to the form control (default: false)

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