diff --git a/README.md b/README.md
index b00c464..2ccb570 100644
--- a/README.md
+++ b/README.md
@@ -36,14 +36,24 @@ export class AppModule {
}
```
-**Credit Card Formater**
+**Credit Card Formatter**
* add `ccNumber` directive:
```html
```
* this will also apply a class name based off the card `.visa`, `.amex`, etc. See the array of card types in `credit-card.ts` for all available types
-**Expiration Date Formater**
+* You can get parsed card type by using export api:
+
+```html
+
+{{ccNumber.resolvedScheme$ | async}}
+```
+
+`resolvedScheme$` will be populated with `visa`, `amex`, etc.
+
+
+**Expiration Date Formatter**
Will support format of MM/YY or MM/YYYY
* add `ccExp` directive:
```html
@@ -96,7 +106,7 @@ export class AppComponent implements OnInit {
# Inspiration
-Based on Stripe's [jquery.payment](https://github.com/stripe/jquery.payment) plugin but adapted for use by Angular2
+Based on Stripe's [jquery.payment](https://github.com/stripe/jquery.payment) plugin but adapted for use by Angular
# License
diff --git a/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.spec.ts b/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.spec.ts
index 7bce233..9b0bfdf 100644
--- a/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.spec.ts
+++ b/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.spec.ts
@@ -1,139 +1,170 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
-import {CreditCardFormatDirective} from './credit-card-format.directive';
+import { CreditCardFormatDirective } from './credit-card-format.directive';
import { Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
-@Component({
- template: ``,
-})
-class TestCreditCardFormatComponent {
+const KEY_MAP = {
+ ONE: 49, // input `1`
+ BACKSPACE: 8,
+};
+
+function createKeyEvent(keyCode: number) {
+ return {keyCode, which: keyCode, preventDefault: jest.fn()};
+}
+
+function triggerKeyEvent(input: DebugElement, eventName: string, keyCode: number) {
+ input.triggerEventHandler(eventName, createKeyEvent(keyCode));
}
+
describe('Directive: CreditCardFormat', () => {
- let component: TestCreditCardFormatComponent;
- let fixture: ComponentFixture;
- let inputEl: DebugElement;
- beforeEach(() => {
- TestBed.configureTestingModule({
- declarations: [TestCreditCardFormatComponent, CreditCardFormatDirective],
+ describe('general cases', () => {
+ @Component({
+ template: ``,
+ })
+ class TestCreditCardFormatComponent {}
+
+ let fixture: ComponentFixture;
+ let inputEl: DebugElement;
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestCreditCardFormatComponent, CreditCardFormatDirective],
+ });
+ fixture = TestBed.createComponent(TestCreditCardFormatComponent);
+ inputEl = fixture.debugElement.query(By.css('input'));
});
- fixture = TestBed.createComponent(TestCreditCardFormatComponent);
- component = fixture.componentInstance;
- inputEl = fixture.debugElement.query(By.css('input'));
- });
- it('formats card number tick by tick', fakeAsync(() => {
+ it('formats card number tick by tick', fakeAsync(() => {
- inputEl.nativeElement.value = '4111 1111';
- inputEl.triggerEventHandler('keydown', {keyCode: 49, which: 49});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111');
+ inputEl.nativeElement.value = '4111 1111';
+ triggerKeyEvent(inputEl, 'keydown', KEY_MAP.ONE);
- inputEl.triggerEventHandler('keypress', {keyCode: 49, which: 49});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111');
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111');
- // the value is changed here by the browser as default behavior
- inputEl.nativeElement.value = '4111 11111';
+ triggerKeyEvent(inputEl, 'keypress', KEY_MAP.ONE);
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111');
- inputEl.nativeElement.focus();
- inputEl.triggerEventHandler('input', null);
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ // the value is changed here by the browser as default behavior
+ inputEl.nativeElement.value = '4111 11111';
- inputEl.triggerEventHandler('keyup', {keyCode: 49, which: 49});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ inputEl.nativeElement.focus();
+ inputEl.triggerEventHandler('input', null);
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ triggerKeyEvent(inputEl, 'keyup', KEY_MAP.ONE);
- }));
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ }));
- it('formats card number one tick', fakeAsync(() => {
+ it('formats card number one tick', fakeAsync(() => {
- inputEl.nativeElement.value = '4111 1111';
- inputEl.triggerEventHandler('keydown', {keyCode: 49, which: 49});
- fixture.detectChanges();
- expect(inputEl.nativeElement.value).toBe('4111 1111');
+ inputEl.nativeElement.value = '4111 1111';
+ inputEl.triggerEventHandler('keydown', {keyCode: KEY_MAP.ONE, which: KEY_MAP.ONE});
+ fixture.detectChanges();
+ expect(inputEl.nativeElement.value).toBe('4111 1111');
- inputEl.triggerEventHandler('keypress', {keyCode: 49, which: 49});
- fixture.detectChanges();
- expect(inputEl.nativeElement.value).toBe('4111 1111');
+ inputEl.triggerEventHandler('keypress', {keyCode: KEY_MAP.ONE, which: KEY_MAP.ONE});
+ fixture.detectChanges();
+ expect(inputEl.nativeElement.value).toBe('4111 1111');
- // the value is changed here by the browser as default behavior
- inputEl.nativeElement.value = '4111 11111';
+ // the value is changed here by the browser as default behavior
+ inputEl.nativeElement.value = '4111 11111';
- inputEl.triggerEventHandler('input', null);
- fixture.detectChanges();
- expect(inputEl.nativeElement.value).toBe('4111 11111');
+ inputEl.triggerEventHandler('input', null);
+ fixture.detectChanges();
+ expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ }));
- inputEl.nativeElement.focus();
- inputEl.triggerEventHandler('keyup', {keyCode: 49, which: 49});
- fixture.detectChanges();
- expect(inputEl.nativeElement.value).toBe('4111 11111');
+ it('deletes from middle of value', fakeAsync(() => {
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111 1');
+ inputEl.nativeElement.value = '4111 1111 111';
+ inputEl.nativeElement.selectionStart = 5;
+ inputEl.nativeElement.selectionEnd = 5;
+ inputEl.nativeElement.focus();
- }));
+ const event = createKeyEvent(KEY_MAP.BACKSPACE);
- it('deletes from middle of value', fakeAsync(() => {
+ inputEl.triggerEventHandler('keydown', event);
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111 11');
+ expect(inputEl.nativeElement.selectionStart).toBe(3);
+ expect(inputEl.nativeElement.selectionEnd).toBe(3);
+ expect(event.preventDefault).toBeCalled();
- inputEl.nativeElement.value = '4111 1111 111';
- inputEl.nativeElement.selectionStart = 5;
- inputEl.nativeElement.selectionEnd = 5;
- inputEl.nativeElement.focus();
+ }));
- let defPrevented = false;
+ it('deletes from beginning of value', fakeAsync(() => {
- inputEl.triggerEventHandler('keydown', {keyCode: 8, which: 8, preventDefault() { defPrevented = true; }});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111 11');
- expect(inputEl.nativeElement.selectionStart).toBe(3);
- expect(inputEl.nativeElement.selectionEnd).toBe(3);
- expect(defPrevented).toBeTruthy();
+ inputEl.nativeElement.value = '5 411 1111';
+ inputEl.nativeElement.selectionStart = 2;
+ inputEl.nativeElement.selectionEnd = 2;
+ inputEl.nativeElement.focus();
- }));
+ const event = createKeyEvent(KEY_MAP.BACKSPACE);
- it('deletes from beginning of value', fakeAsync(() => {
+ inputEl.triggerEventHandler('keydown', event);
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 111');
+ expect(inputEl.nativeElement.selectionStart).toBe(0);
+ expect(inputEl.nativeElement.selectionEnd).toBe(0);
+ expect(event.preventDefault).toBeCalled();
- inputEl.nativeElement.value = '5 411 1111';
- inputEl.nativeElement.selectionStart = 2;
- inputEl.nativeElement.selectionEnd = 2;
- inputEl.nativeElement.focus();
+ }));
- let defPrevented = false;
+ it('does not modify deleting from end of value', fakeAsync(() => {
- inputEl.triggerEventHandler('keydown', {keyCode: 8, which: 8, preventDefault() { defPrevented = true; }});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 111');
- expect(inputEl.nativeElement.selectionStart).toBe(0);
- expect(inputEl.nativeElement.selectionEnd).toBe(0);
- expect(defPrevented).toBeTruthy();
+ inputEl.nativeElement.value = '4111 1111 111';
+ inputEl.nativeElement.selectionStart = 13;
+ inputEl.nativeElement.selectionEnd = 13;
+ inputEl.nativeElement.focus();
- }));
+ const event = createKeyEvent(KEY_MAP.BACKSPACE);
+ inputEl.triggerEventHandler('keydown', event);
+ fixture.detectChanges();
+ tick(10);
+ expect(inputEl.nativeElement.value).toBe('4111 1111 111');
+ expect(inputEl.nativeElement.selectionStart).toBe(13);
+ expect(inputEl.nativeElement.selectionEnd).toBe(13);
+ expect(event.preventDefault).not.toBeCalled();
- it('does not modify deleting from end of value', fakeAsync(() => {
-
- inputEl.nativeElement.value = '4111 1111 111';
- inputEl.nativeElement.selectionStart = 13;
- inputEl.nativeElement.selectionEnd = 13;
- inputEl.nativeElement.focus();
+ }));
+ });
- let defPrevented = false;
+ describe('exportAs cases', () => {
+ @Component({
+ template: `
+ {{ccNumber.resolvedScheme$ | async}}`,
+ })
+ class TestCreditCardFormatComponent {}
+
+ let fixture: ComponentFixture;
+ let inputEl: DebugElement;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestCreditCardFormatComponent, CreditCardFormatDirective],
+ });
+ fixture = TestBed.createComponent(TestCreditCardFormatComponent);
+ inputEl = fixture.debugElement.query(By.css('input'));
+ });
- inputEl.triggerEventHandler('keydown', {keyCode: 8, which: 8, preventDefault() { defPrevented = true; }});
- fixture.detectChanges();
- tick(10);
- expect(inputEl.nativeElement.value).toBe('4111 1111 111');
- expect(inputEl.nativeElement.selectionStart).toBe(13);
- expect(inputEl.nativeElement.selectionEnd).toBe(13);
- expect(defPrevented).toBeFalsy();
+ it('should provide resolved scheme via exportAs', () => {
+ (inputEl.nativeElement as HTMLInputElement).value = '4111111111111111';
+ inputEl.triggerEventHandler('input', null);
+ fixture.detectChanges();
- }));
+ const span: HTMLSpanElement = fixture.debugElement.query(By.css('.scheme')).nativeElement;
+ expect(span.textContent).toBe('visa');
+ });
+ });
});
diff --git a/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.ts b/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.ts
index 7886c7e..2a35f8b 100644
--- a/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.ts
+++ b/projects/angular-cc-library/src/lib/directives/credit-card-format.directive.ts
@@ -1,14 +1,18 @@
import { Directive, ElementRef, HostListener, Optional, Self } from '@angular/core';
import { CreditCard } from '../credit-card';
import { NgControl } from '@angular/forms';
+import { BehaviorSubject } from 'rxjs';
@Directive({
- selector: '[ccNumber]',
+ selector: 'input[ccNumber]',
+ exportAs: 'ccNumber',
})
export class CreditCardFormatDirective {
- public target: HTMLInputElement;
+ private target: HTMLInputElement;
private cards = CreditCard.cards();
+ public resolvedScheme$ = new BehaviorSubject('unknown');
+
constructor(
private el: ElementRef,
@Self() @Optional() private control: NgControl,
@@ -67,28 +71,15 @@ export class CreditCardFormatDirective {
}
private formatCardNumber(e: KeyboardEvent) {
- let card;
- let digit;
- let length;
- let upperLength;
- let value;
-
- digit = String.fromCharCode(e.which);
+ const digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
- value = this.target.value;
-
- card = CreditCard.cardFromNumber(value + digit);
-
- length = (value.replace(/\D/g, '') + digit).length;
-
- upperLength = 19;
-
- if (card) {
- upperLength = card.length[card.length.length - 1];
- }
+ const value = this.target.value;
+ const card = CreditCard.cardFromNumber(value + digit);
+ const length = (value.replace(/\D/g, '') + digit).length;
+ const upperLength = card ? card.length[card.length.length - 1] : 19;
if (length >= upperLength) {
return;
@@ -122,15 +113,14 @@ export class CreditCardFormatDirective {
}
private setCardType() {
- let card;
- const val = this.target.value;
- const cardType = CreditCard.cardType(val) || 'unknown';
+ const cardType = CreditCard.cardType(this.target.value) || 'unknown';
+
+ this.resolvedScheme$.next(cardType);
if (!this.target.classList.contains(cardType)) {
- for (let i = 0, len = this.cards.length; i < len; i++) {
- card = this.cards[i];
+ this.cards.forEach((card) => {
this.target.classList.remove(card.type);
- }
+ });
this.target.classList.remove('unknown');
this.target.classList.add(cardType);
@@ -139,15 +129,14 @@ export class CreditCardFormatDirective {
}
private reFormatCardNumber() {
- setTimeout(() => {
- let value = CreditCard.replaceFullWidthChars(this.target.value);
- value = CreditCard.formatCardNumber(value);
- const oldValue = this.target.value;
- if (value !== oldValue) {
- this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(value, this.target, (safeVal => {
- this.updateValue(safeVal);
- }));
- }
- });
+ const value = CreditCard.formatCardNumber(
+ CreditCard.replaceFullWidthChars(this.target.value),
+ );
+ const oldValue = this.target.value;
+ if (value !== oldValue) {
+ this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(value, this.target, (safeVal => {
+ this.updateValue(safeVal);
+ }));
+ }
}
}
diff --git a/projects/angular-cc-library/src/lib/directives/cvc-format.directive.ts b/projects/angular-cc-library/src/lib/directives/cvc-format.directive.ts
index 197886b..cf62aff 100644
--- a/projects/angular-cc-library/src/lib/directives/cvc-format.directive.ts
+++ b/projects/angular-cc-library/src/lib/directives/cvc-format.directive.ts
@@ -3,11 +3,10 @@ import { CreditCard } from '../credit-card';
import { NgControl } from '@angular/forms';
@Directive({
- selector: '[ccCVC]',
+ selector: 'input[ccCVC]',
})
-
export class CvcFormatDirective {
- public target: HTMLInputElement;
+ private target: HTMLInputElement;
constructor(
private el: ElementRef,
@@ -36,28 +35,17 @@ export class CvcFormatDirective {
}
@HostListener('paste')
- public onPaste() {
- this.reformatCvc();
- }
@HostListener('change')
- public onChange() {
- this.reformatCvc();
- }
@HostListener('input')
- public onInput() {
- this.reformatCvc();
- }
-
- private reformatCvc() {
- setTimeout(() => {
- let val = CreditCard.replaceFullWidthChars(this.target.value);
- val = val.replace(/\D/g, '').slice(0, 4);
- const oldVal = this.target.value;
- if (val !== oldVal) {
- this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => {
- this.updateValue(safeVal);
- }));
- }
- });
+ public reformatCvc() {
+ const val = CreditCard.replaceFullWidthChars(this.target.value)
+ .replace(/\D/g, '')
+ .slice(0, 4);
+ const oldVal = this.target.value;
+ if (val !== oldVal) {
+ this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => {
+ this.updateValue(safeVal);
+ }));
+ }
}
}
diff --git a/projects/angular-cc-library/src/lib/directives/expiry-format.directive.ts b/projects/angular-cc-library/src/lib/directives/expiry-format.directive.ts
index 9cf7b3c..83de034 100644
--- a/projects/angular-cc-library/src/lib/directives/expiry-format.directive.ts
+++ b/projects/angular-cc-library/src/lib/directives/expiry-format.directive.ts
@@ -3,10 +3,10 @@ import { CreditCard } from '../credit-card';
import { NgControl } from '@angular/forms';
@Directive({
- selector: '[ccExp]',
+ selector: 'input[ccExp]',
})
export class ExpiryFormatDirective {
- public target: HTMLInputElement;
+ private target: HTMLInputElement;
constructor(
private el: ElementRef,
@@ -63,23 +63,22 @@ export class ExpiryFormatDirective {
const val = `${this.target.value}${digit}`;
if (!/^\d+$/.test(digit)) {
- if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
- e.preventDefault();
- setTimeout(() => {
- this.updateValue(`0${val} / `);
- });
- } else if (/^\d\d$/.test(val)) {
- e.preventDefault();
- setTimeout(() => {
- const m1 = parseInt(val[0], 10);
- const m2 = parseInt(val[1], 10);
- if (m2 > 2 && m1 !== 0) {
- this.updateValue(`0${m1} / ${m2}`);
- } else {
- this.updateValue(`${val} / `);
- }
- });
+ return;
+ }
+
+ if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
+ e.preventDefault();
+ this.updateValue(`0${val} / `);
+ } else if (/^\d\d$/.test(val)) {
+ e.preventDefault();
+ const m1 = parseInt(val[0], 10);
+ const m2 = parseInt(val[1], 10);
+ if (m2 > 2 && m1 !== 0) {
+ this.updateValue(`0${m1} / ${m2}`);
+ } else {
+ this.updateValue(`${val} / `);
}
+
}
}
@@ -115,24 +114,20 @@ export class ExpiryFormatDirective {
}
if (/\d\s\/\s$/.test(val)) {
e.preventDefault();
- setTimeout(() => {
- this.updateValue(val.replace(/\d\s\/\s$/, ''));
- });
+ this.updateValue(val.replace(/\d\s\/\s$/, ''));
}
}
private reformatExpiry() {
- setTimeout(() => {
- let val = this.target.value;
- val = CreditCard.replaceFullWidthChars(val);
- val = CreditCard.formatExpiry(val);
- const oldVal = this.target.value;
- if (val !== oldVal) {
- this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => {
- this.updateValue(safeVal);
- }));
- }
- });
+ const val = CreditCard.formatExpiry(
+ CreditCard.replaceFullWidthChars(this.target.value),
+ );
+
+ const oldVal = this.target.value;
+ if (val !== oldVal) {
+ this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => {
+ this.updateValue(safeVal);
+ }));
+ }
}
-
}
diff --git a/tslint.json b/tslint.json
index 7b3cfb5..ccdd5f6 100644
--- a/tslint.json
+++ b/tslint.json
@@ -1,6 +1,7 @@
{
"extends": "tslint:recommended",
"rules": {
+ "ban": [true, "fdescribe"],
"array-type": false,
"arrow-parens": false,
"deprecation": false,