Loop through the credit card number and manually add spaces
This works in Angular 7+ and probably earlier versions too.
We can just loop through the numbers and add a space every four digits for most Credit Card types, but in a 4-6-5 pattern for American Express.
The only tricky part is handling Backspace and Cursor Position editing (ie if the user clicks inside the input to edit it). If we don't handle this, the loop will make editing the number a weird UX headache.
You can write a custom component or directive for this, but no need to do so if you only have one component in your application that is taking credit card numbers, which is often the case. (Put this code in a directive if you have multiple credit card inputs in your application.)
Here's how I did it inside the payment component in my app:
- Create a variable called
partitions
to handle the spacing format. Set it to 4-6-5 for Amex and default to 4-4-4-4 for all other cards. We'll loop through these partitions as we add spaces.
- Use a template reference variable
#ccNumber
for cursor position detection.
- To handle backspace and cursor arrow keys, we store the original cursor position and restore it after editing from a spot anywhere other than the end of the string.
/* Insert spaces to enhance legibility of credit card numbers */
creditCardNumberSpacing() {
const input = this.ccNumberField.nativeElement;
const { selectionStart } = input;
const { cardNumber } = this.paymentForm.controls;
let trimmedCardNum = cardNumber.value.replace(/\s+/g, '');
if (trimmedCardNum.length > 16) {
trimmedCardNum = trimmedCardNum.substr(0, 16);
}
/* Handle American Express 4-6-5 spacing */
const partitions = trimmedCardNum.startsWith('34') || trimmedCardNum.startsWith('37')
? [4,6,5]
: [4,4,4,4];
const numbers = [];
let position = 0;
partitions.forEach(partition => {
const part = trimmedCardNum.substr(position, partition);
if (part) numbers.push(part);
position += partition;
})
cardNumber.setValue(numbers.join(' '));
/* Handle caret position if user edits the number later */
if (selectionStart < cardNumber.value.length - 1) {
input.setSelectionRange(selectionStart, selectionStart, 'none');
}
}
(If you have a routine of your own to detect American Express numbers, use it. What I'm using here simply examines the first two digits and compares them to PAN/IIN standards.)
Higher in your component, ensure you have the right imports:
import { ViewChild, ElementRef } from '@angular/core';
And:
@ViewChild('ccNumber') ccNumberField: ElementRef;
And when you set up your form controls, do this so that spaces can be included in your regex pattern:
this.paymentForm = this.fb.group({
cardNumber: ['', [Validators.required, Validators.pattern('^[ 0-9]*$';), Validators.minLength(17)]]
})
And finally, in your template, configure your element like this:
<input maxlength="20"
formControlName="cardNumber"
type="tel"
#ccNumber
(keyup)="creditCardNumberSpacing()">
You should be good to go. Spaces will automatically appear as the user enters their number, and they can still go back and edit if they make typos.
PS: If you want to format with dashes (-
) instead of spaces, replace the space with a dash in the cardNumber.setValue(numbers.join(' '))
line, and in the regex adjustment.