credit-card.component.ts (3238B)
1 import { CommonModule } from '@angular/common'; 2 import { Component, computed, signal } from '@angular/core'; 3 4 const SPECIAL_KEYS = ['Backspace', 'Tab', 'End', 'Home', 'ArrowLeft', 'ArrowRight', 'Delete', 'Enter']; 5 @Component({ 6 selector: 'div.card-input', 7 standalone: true, 8 template: ` 9 <input 10 type="text" 11 maxlength="19" 12 placeholder="XXXX-XXXX-XXXX-XXXX" 13 [value]="format()" 14 (input)="handleInput($event)" 15 inputmode="numeric" 16 class="form-control" 17 [class.is-invalid]="showValidation() && !isValidCard()" 18 [class.is-valid]="showValidation() && isValidCard()" 19 (keydown)="validKey($event) || $event.preventDefault()" 20 (blur)="onTouched()" 21 autocomplete="off" 22 /> 23 24 <!-- Feedback Icon Container (Uses Bootstrap Icons) --> 25 @if (showValidation() && !isValidCard()) { 26 <div class="invalid-feedback d-block"> 27 <i class="bi bi-exclamation-circle me-1"></i> 28 Invalid card number 29 </div> 30 } 31 `, 32 styles: ` 33 input { 34 font-size: .85rem; 35 } 36 `, 37 imports: [CommonModule], 38 }) 39 export class CardInputComponent { 40 private rawDigits = signal(''); 41 private touched = signal(false); 42 43 // --- Computed Signals for State Management --- 44 45 // Formats the raw digits with hyphens (e.g., "1234-5678-...") 46 format = computed(() => { 47 const digits = this.rawDigits(); 48 // Trim to maximum 16 digits and insert hyphens every 4 characters 49 return ( 50 digits 51 .substring(0, 16) 52 .match(/.{1,4}/g) 53 ?.join('-') || '' 54 ); 55 }); 56 57 // True if the input has exactly 16 clean digits 58 isValidCard = computed(() => this.rawDigits().length === 16); 59 60 // Show validation feedback when touched and has input 61 showValidation = computed(() => this.touched() && this.rawDigits().length > 0); 62 63 /** 64 * Handles the input event, cleans the value, and updates the signal. 65 * @param event The native input event from the template. 66 */ 67 handleInput(event: Event): void { 68 const inputEle = event.target as HTMLInputElement; 69 const value = inputEle.value; 70 const cleaned = value.replace(/\D/g, ''); 71 const limitedDigits = cleaned.substring(0, 16); 72 this.rawDigits.set(limitedDigits); 73 } 74 75 validKey(event: KeyboardEvent): boolean { 76 const { key } = event; 77 return /^\d$/.test(key) || SPECIAL_KEYS.includes(key); 78 } 79 80 onTouched(): void { 81 this.touched.set(true); 82 } 83 84 // Public method to mark as touched from parent component 85 markAsTouched(): void { 86 this.touched.set(true); 87 } 88 89 // Get raw card number (for extracting last 4 digits and detecting brand) 90 getCardNumber(): string { 91 return this.rawDigits(); 92 } 93 94 // Get last 4 digits of card 95 getCardLast4(): string { 96 const digits = this.rawDigits(); 97 return digits.length >= 4 ? digits.slice(-4) : ''; 98 } 99 100 // Detect card brand from card number 101 getCardBrand(): string { 102 const digits = this.rawDigits(); 103 if (digits.length === 0) return ''; 104 105 const firstDigit = digits[0]; 106 if (firstDigit === '4') return 'Visa'; 107 if (firstDigit === '5') return 'Mastercard'; 108 if (firstDigit === '3') return 'American Express'; 109 if (firstDigit === '6') return 'Discover'; 110 111 return 'Unknown'; 112 } 113 }