Don't forget to handle Backspace, Cursor Position, and American Express
I had to handle some extra complexity, but it's likely what many people will confront when developing this. We need to consider use of the Backspace
key and arrow keys when rewriting the input value. There is also American Express numbers to consider, which are not simply 4-4-4-4 numbers.
Here's how I did it in a component using a template reference variable and cursor position detection. (No need for a custom directive if you only have one component that is taking credit card numbers, which is often the case.)
To handle Backspace
and cursor arrow keys, we have to store the original cursor position and restore it after editing from a spot anywhere other than the end of the string.
To enable handling of American Express, I use a variable called partitions
to store the 4-6-5 spacing format for Amex and the 4-4-4-4 spacing format for all other cards. We loop partitions
as we add spaces.
/* Insert spaces to make CC number more legible */
cardNumberSpacing() {
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 format */
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 something like 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)="cardNumberSpacing()">
You should be good to go!