exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }