Last active
June 25, 2025 14:43
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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}`; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 inmaskPattern
How to Usage?
Can be used with Angular Reactive Forms.
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)