projects/ng-payment-card/src/lib/payment-card.component.ts
NgPaymentCard without any dependencies other then ReactiveFormsModule
encapsulation | ViewEncapsulation.None |
selector | ng-payment-card |
styleUrls | ./payment-card.component.scss |
templateUrl | ./payment-card.component.html |
constructor(_ccService: PaymentCardService, _fb: FormBuilder)
|
|||||||||
Parameters :
|
cardExpiredTxt | |
Default value : 'Card has expired'
|
|
Validation message for expired card |
cardHolderMissingTxt | |
Default value : 'Card holder name is required'
|
|
Validation message for missing card holder name |
cardHolderTooLongTxt | |
Default value : 'Card holder name is too long'
|
|
Validation message for too long card holder name |
ccNumChecksumInvalidTxt | |
Default value : 'Provided card number is invalid'
|
|
Validation message for invalid payment card number (Luhn's validation) |
ccNumContainsLettersTxt | |
Default value : 'Card number can contain digits only'
|
|
Validation message for payment card number that contains characters other than digits |
ccNumMissingTxt | |
Default value : 'Card number is required'
|
|
Validation message for missing payment card number |
ccNumTooLongTxt | |
Default value : 'Card number is too long'
|
|
Validation message for too long payment card number |
ccNumTooShortTxt | |
Default value : 'Card number is too short'
|
|
Validation message for too short payment card number |
ccvContainsLettersTxt | |
Default value : 'CCV number can contain digits only'
|
|
Validation message for incorrect CCV number containing characters other than digits |
ccvMissingTxt | |
Default value : 'CCV number is required'
|
|
Validation message for missing CCV number |
ccvNumTooLongTxt | |
Default value : 'CCV number is too long'
|
|
Validation message for too long CCV number |
ccvNumTooShortTxt | |
Default value : 'CCV number is too short'
|
|
Validation message for too short CCV number |
expirationMonthMissingTxt | |
Default value : 'Expiration month is required'
|
|
Validation message for missing expiration month |
expirationYearMissingTxt | |
Default value : 'Expiration year is required'
|
|
Validation message for missing expiration year |
validateCardExpiration | |
Default value : true
|
|
Switch validation of the payment card expiration |
validateCardHolder | |
Default value : true
|
|
Switch validation of the payment card holder |
validateCCNum | |
Default value : true
|
|
Switch validation of the payment card number |
validateCCV | |
Default value : true
|
|
Switch validation of the payment card CCV number |
validateExpirationMonth | |
Default value : true
|
|
Switch validation of the payment card expiration month |
validateExpirationYear | |
Default value : true
|
|
Switch validation of the payment card expiration year |
formSaved | |
Type : EventEmitter<ICardDetails>
|
|
EventEmitter for payment card object |
Private assignDateValues |
assignDateValues()
|
Populate months and years
Returns :
void
|
Private buildForm |
buildForm()
|
Build reactive form
Returns :
void
|
Public emitSavedCard |
emitSavedCard()
|
Callback function that emits payment card details after user clicks submit, or press enter
Returns :
void
|
Public getCardType | ||||||
getCardType(ccNum: string)
|
||||||
Returns payment card type based on payment card number
Parameters :
Returns :
string | null
|
Public ngOnInit |
ngOnInit()
|
Returns :
void
|
Public ccForm |
Type : FormGroup
|
FormGroup available publicly |
Public months |
Type : Array<string>
|
Default value : []
|
List of months |
Public years |
Type : Array<number>
|
Default value : []
|
List of years |
import { Component, EventEmitter, OnInit, Output, Input, ViewEncapsulation } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { CardValidator } from './validator/card-validator';
import { ICardDetails } from './domain/i-card-details';
import { CardDetails } from './domain/card-details';
import { PaymentCardService } from './service/payment-card.service';
/**
* NgPaymentCard without any dependencies other then ReactiveFormsModule
*/
@Component({
selector: 'ng-payment-card',
templateUrl: './payment-card.component.html',
styleUrls: ['./payment-card.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class PaymentCardComponent implements OnInit {
/**
* FormGroup available publicly
*/
public ccForm: FormGroup;
/**
* List of months
*/
public months: Array<string> = [];
/**
* List of years
*/
public years: Array<number> = [];
/**
* Validation message for missing payment card number
*/
@Input()
public ccNumMissingTxt? = 'Card number is required';
/**
* Validation message for too short payment card number
*/
@Input()
public ccNumTooShortTxt? = 'Card number is too short';
/**
* Validation message for too long payment card number
*/
@Input()
public ccNumTooLongTxt? = 'Card number is too long';
/**
* Validation message for payment card number that contains characters other than digits
*/
@Input()
public ccNumContainsLettersTxt? = 'Card number can contain digits only';
/**
* Validation message for invalid payment card number (Luhn's validation)
*/
@Input()
public ccNumChecksumInvalidTxt? = 'Provided card number is invalid';
/**
* Validation message for missing card holder name
*/
@Input()
public cardHolderMissingTxt? = 'Card holder name is required';
/**
* Validation message for too long card holder name
*/
@Input()
public cardHolderTooLongTxt? = 'Card holder name is too long';
/**
* Validation message for missing expiration month
*/
@Input()
public expirationMonthMissingTxt? = 'Expiration month is required';
/**
* Validation message for missing expiration year
*/
@Input()
public expirationYearMissingTxt? = 'Expiration year is required';
/**
* Validation message for missing CCV number
*/
@Input()
public ccvMissingTxt? = 'CCV number is required';
/**
* Validation message for too short CCV number
*/
@Input()
public ccvNumTooShortTxt? = 'CCV number is too short';
/**
* Validation message for too long CCV number
*/
@Input()
public ccvNumTooLongTxt? = 'CCV number is too long';
/**
* Validation message for incorrect CCV number containing characters other than digits
*/
@Input()
public ccvContainsLettersTxt? = 'CCV number can contain digits only';
/**
* Validation message for expired card
*/
@Input()
public cardExpiredTxt? = 'Card has expired';
/**
* Switch validation of the payment card number
*/
@Input()
public validateCCNum? = true;
/**
* Switch validation of the payment card holder
*/
@Input()
public validateCardHolder? = true;
/**
* Switch validation of the payment card expiration month
*/
@Input()
public validateExpirationMonth? = true;
/**
* Switch validation of the payment card expiration year
*/
@Input()
public validateExpirationYear? = true;
/**
* Switch validation of the payment card expiration
*/
@Input()
public validateCardExpiration? = true;
/**
* Switch validation of the payment card CCV number
*/
@Input()
public validateCCV? = true;
/**
* EventEmitter for payment card object
*/
@Output()
public formSaved: EventEmitter<ICardDetails> = new EventEmitter<CardDetails>();
constructor(private _ccService: PaymentCardService, private _fb: FormBuilder) {}
public ngOnInit(): void {
this.buildForm();
this.assignDateValues();
}
/**
* Populate months and years
*/
private assignDateValues(): void {
this.months = PaymentCardService.getMonths();
this.years = PaymentCardService.getYears();
}
/**
* Build reactive form
*/
private buildForm(): void {
this.ccForm = this._fb.group(
{
cardNumber: [
'',
Validators.compose([
Validators.required,
Validators.minLength(12),
Validators.maxLength(19),
CardValidator.numbersOnly,
CardValidator.checksum,
]),
],
cardHolder: ['', Validators.compose([Validators.required, Validators.maxLength(22)])],
expirationMonth: ['', Validators.required],
expirationYear: ['', Validators.required],
ccv: [
'',
Validators.compose([
Validators.required,
Validators.minLength(3),
Validators.maxLength(4),
CardValidator.numbersOnly,
]),
],
},
{
validator: CardValidator.expiration,
}
);
}
/**
* Returns payment card type based on payment card number
*/
public getCardType(ccNum: string): string | null {
return PaymentCardService.getCardType(ccNum);
}
/**
* Callback function that emits payment card details after user clicks submit, or press enter
*/
public emitSavedCard(): void {
const cardDetails: ICardDetails = <CardDetails>this.ccForm.value;
this.formSaved.emit(cardDetails);
}
}
<section class="cc-wrapper">
<div class="cc-box">
<div #ccBoxFlip class="cc-box--flip">
<div class="cc-box__front">
<div class="cc-box__logo">
<p>{{getCardType(ccNumber.value) | uppercase}}</p>
</div>
<div class="cc-box__element">
<label class="cc-form__label" for="cc-card-number-display"></label>
<input class="cc-form__input cc-form__input--transparent cc-form__input--embosed" id="cc-card-number-display"
aria-label="Payment card number" disabled="disabled"
[value]="ccForm.get('cardNumber').value | paymentCardNumber">
</div>
<div class="cc-box__element">
<label class="cc-form__label" for="cc-holder-display">CARD HOLDER</label>
<input class="cc-form__input cc-form__input--transparent cc-form__input--embosed" id="cc-holder-display"
aria-label="Card holder" disabled="disabled" [value]="ccForm.get('cardHolder').value | uppercase">
</div>
<div class="cc-box__element">
<label class="cc-form__label" for="cc-valid-date-display">VALID THRU</label>
<input class="cc-form__input cc-form__input--left-align cc-form__input--transparent cc-form__input--embosed"
id="cc-valid-date-display" aria-label="Card holder" disabled="disabled"
[value]="ccForm.get('expirationMonth').value + '/' + ccForm.get('expirationYear').value | validThru">
</div>
<div class="cc-box__chip"></div>
</div>
<div class="cc-box__back">
<div class="cc-box__strip"> </div>
<div class="cc-box__element">
<input class="cc-form__input cc-form__input--cursive cc-form__input--right-align" id="cc-ccv-display"
aria-label="CCV" disabled="disabled"
[value]="'CCV: ' + ccForm.get('ccv').value" title="CCV">
</div>
</div>
</div>
</div>
<form class="cc-form" [formGroup]="ccForm" autocomplete="off">
<div class="cc-form__wrapper--long">
<label for="cc-number" class="cc-form__label cc-form__label--first">Card number</label>
<input #ccNumber class="cc-form__input" id="cc-number" aria-label="Card number"
type="text" title="Card number" maxlength="19" formControlName="cardNumber"
(focus)="ccBoxFlip.classList.remove('hover')">
<div class="cc-form__error"
*ngIf="validateCCNum && ccForm.get('cardNumber').touched && ccForm.get('cardNumber').hasError('required')">
{{ccNumMissingTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCNum && ccForm.get('cardNumber').touched && ccForm.get('cardNumber').hasError('minlength')">
{{ccNumTooShortTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCNum && ccForm.get('cardNumber').touched && ccForm.get('cardNumber').hasError('maxlength')">
{{ccNumTooLongTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCNum && ccForm.get('cardNumber').touched && ccForm.get('cardNumber').hasError('numbersOnly')">
{{ccNumContainsLettersTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCNum && ccForm.get('cardNumber').touched && ccForm.get('cardNumber').hasError('checksum')">
{{ccNumChecksumInvalidTxt}}
</div>
</div>
<div class="cc-form__wrapper--long">
<label for="cc-holder-name" class="cc-form__label">Card Holder name</label>
<input class="cc-form__input" id="cc-holder-name" aria-label="Card holder name" type="text"
title="Card holder name" maxlength="22" formControlName="cardHolder"
(focus)="ccBoxFlip.classList.remove('hover')">
<div class="cc-form__error"
*ngIf="validateCardHolder && ccForm.get('cardHolder').touched &&
ccForm.get('cardHolder').hasError('required')">
{{cardHolderMissingTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCardHolder && ccForm.get('cardHolder').touched &&
ccForm.get('cardHolder').hasError('maxlength')">
{{cardHolderTooLongTxt}}
</div>
</div>
<div class="cc-form--inline">
<div class="cc-form__wrapper cc-form__wrapper--short">
<label for="cc-expiration-month" class="cc-form__label">Expiration month</label>
<select id="cc-expiration-month" class="cc-form__select" aria-label="Expiration month"
formControlName="expirationMonth">
<option *ngFor="let month of months" value="{{month}}"
(click)="ccBoxFlip.classList.remove('hover')">{{month}}
</option>
</select>
</div>
<div class="cc-form__wrapper cc-form__wrapper--short">
<label for="cc-expiration-year" class="cc-form__label">Expiration year</label>
<select id="cc-expiration-year" class="cc-form__select" aria-label="Expiration year"
formControlName="expirationYear">
<option *ngFor="let year of years" value="{{year}}"
(click)="ccBoxFlip.classList.remove('hover')">{{year}}
</option>
</select>
</div>
<div class="cc-form__wrapper cc-form__wrapper--short cc-form__wrapper--last">
<label for="cc-ccv" class="cc-form__label">ccv</label>
<input class="cc-form__input cc-form__input--short" id="cc-ccv" aria-label="CCV" type="text" title="CCV"
minlength="3" maxlength="4" formControlName="ccv" (focus)="ccBoxFlip.classList.add('hover')">
</div>
</div>
<div class="cc-form__error"
*ngIf="validateExpirationMonth && ccForm.get('expirationMonth').touched &&
ccForm.get('expirationMonth').hasError('required')">
{{expirationMonthMissingTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateExpirationMonth && ccForm.get('expirationYear').touched &&
ccForm.get('expirationYear').hasError('required')">
{{expirationYearMissingTxt}}
</div>
<div class="cc-form__error" *ngIf="validateCardExpiration && ccForm.get('expirationMonth').touched &&
ccForm.get('expirationYear').touched && ccForm.hasError('expiration')">
{{cardExpiredTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCV && ccForm.get('ccv').touched && ccForm.get('ccv').hasError('required')">
{{ccvMissingTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCV && ccForm.get('ccv').touched && ccForm.get('ccv').hasError('minlength')">
{{ccvNumTooShortTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCV && ccForm.get('ccv').touched && ccForm.get('ccv').hasError('maxlength')">
{{ccvNumTooLongTxt}}
</div>
<div class="cc-form__error"
*ngIf="validateCCV && ccForm.get('ccv').touched && ccForm.get('ccv').hasError('numbersOnly')">
{{ccvContainsLettersTxt}}
</div>
<button type="submit" class="cc-form__button cc-form__button--ripple" aria-label="submit" (click)="emitSavedCard()"
(keydown.enter)="emitSavedCard()">Submit
</button>
</form>
</section>
./payment-card.component.scss
@import 'style/manifest';
.cc-form {
align-items: center;
display: flex;
flex-flow: column;
flex-wrap: wrap;
height: 100%;
justify-content: center;
width: 100%;
&--inline {
align-items: inherit;
display: inherit;
flex-flow: row;
flex-wrap: inherit;
height: 100%;
justify-content: flex-end;
margin-bottom: $small-margin;
width: 100%;
@include breakpoint(tablet) {
align-items: inherit;
display: inherit;
flex-flow: row;
flex-wrap: inherit;
height: 100%;
justify-content: flex-end;
margin-bottom: $small-margin;
width: 100%;
}
@include breakpoint(mobile) {
align-items: center;
display: inherit;
flex-flow: column;
flex-wrap: wrap;
height: 100%;
justify-content: center;
width: 100%;
}
}
@at-root input {
&[type='number'] {
-moz-appearance: textfield;
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
&__wrapper {
@include breakpoint(laptop) {
margin-right: $small-margin;
margin-top: $small-margin;
&--long {
width: $input-width-large;
}
&--short {
width: $input-width-short;
}
&--last {
margin-left: $small-margin;
margin-right: $big-margin;
margin-top: $label-line-height + 20px;
}
}
@include breakpoint(tablet) {
margin-right: $small-margin;
margin-top: $small-margin;
&--long {
width: $input-width-large;
}
&--short {
width: $input-width-short;
}
&--last {
margin-left: $small-margin;
margin-right: $big-margin;
}
}
@include breakpoint(mobile) {
margin-right: 0;
margin-top: 0;
&--long,
&--short {
width: 80%;
}
&--last {
margin-left: 0;
margin-right: 0;
}
}
}
&__label {
color: hsla(0, 0, 0, 0.6);
display: block;
font-family: 'Inconsolata', Serif, serif;
font-size: $label-font-size-normal;
font-weight: normal;
letter-spacing: 1px;
line-height: $label-line-height;
margin-bottom: 5px;
margin-top: $small-margin;
text-align: left;
text-shadow: none;
text-transform: uppercase;
width: 100%;
&--first {
margin-top: $big-margin;
}
}
&__select {
@extend .cc-form__input;
appearance: listbox;
}
&__input {
border: 1px solid hsla(0, 0, 0, 0.3);
border-radius: 5px;
box-shadow: inset 0 1px 4px hsla(0, 0, 0, 0.2);
color: hsl(0, 0, 20);
display: block;
font-size: $input-font-size;
height: $input-height;
margin: 0;
outline: none;
padding: 0;
text-align: left;
width: 100%;
&--transparent {
background: transparent;
border: none;
border-radius: 0;
box-shadow: none;
}
&--embosed {
color: $card-font-colour;
font-family: 'Inconsolata', monospace;
font-size: $cc-font-size;
text-shadow: 0 2px 1px hsla(0, 0, 0, 0.3);
@include breakpoint(tablet) {
font-size: $cc-font-size-medium;
}
@include breakpoint(mobile) {
font-size: $cc-font-size-big;
}
}
&--cursive {
font-size: $label-font-size-normal;
font-style: italic;
left: 0;
margin: 0 auto;
position: absolute;
}
&--right-align {
padding-right: $small-margin;
text-align: right;
}
&--left-align {
text-align: left;
}
&:focus {
border-color: $input-focus-border-colour;
}
}
&__error {
color: $card-font-invalid-colour;
font-size: $label-font-size-normal;
}
&__button {
background: $input-focus-border-colour;
border: 0;
border-radius: $small-radius;
color: $card-font-colour;
cursor: pointer;
margin-bottom: $small-margin;
margin-top: $small-padding;
outline: 0;
overflow: hidden;
padding: $small-padding;
position: relative;
text-align: center;
user-select: none;
vertical-align: middle;
white-space: nowrap;
width: $input-width-large;
&:hover {
box-shadow: 0 6px 8px -3px rgba(0, 0, 0, 0.3);
}
&:focus {
background: darken($input-focus-border-colour, 12%);
}
&--ripple {
overflow: hidden;
position: relative;
&:after {
background: rgba(255, 255, 255, 0.3);
border-radius: 80%;
content: '';
display: block;
height: 120px;
left: 50%;
margin-left: -50%;
margin-top: -60px;
position: absolute;
top: 50%;
transform: scale(0);
width: 100%;
}
&:not(:active):after {
animation: button-ripple 2s ease-out;
}
}
@keyframes button-ripple {
0% {
transform: scale(0);
}
20% {
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1);
}
}
}
}
.cc-wrapper {
background-color: $wrapper-background;
border-radius: $wrapper-radius;
height: 100%;
margin: 0;
padding: 0;
width: 100%;
}
.cc-box {
height: $card-height;
margin: 0 auto;
padding: 0;
position: relative;
transform: translateY(-100%);
width: $card-width;
z-index: 1;
@include breakpoint(tablet) {
transform: translateY(-115%);
}
@include breakpoint(mobile) {
transform: translateY(-130%);
}
&--flip {
transform-style: preserve-3d;
transition: $transition-time;
&:hover,
&.hover {
transform: rotateY(180deg);
}
}
&__element {
padding: 0;
width: 80%;
}
&__logo {
align-items: center;
color: $card-font-colour;
display: flex;
flex-flow: row nowrap;
font-size: $input-font-size;
font-style: italic;
font-weight: bold;
justify-content: flex-end;
margin-right: $medium-margin;
width: 100%;
}
&__strip {
background: linear-gradient(135deg, hsl(0, 0, 25%), hsl(0, 0, 10%));
font-size: $strip-font-size;
margin: 0;
padding: 0;
position: relative;
transform: translateY(-90%);
width: 100%;
}
&__front,
&__back {
align-items: center;
backface-visibility: hidden;
background: linear-gradient(135deg, #bd6772, #53223f);
border-radius: 15px;
display: flex;
flex-direction: column;
flex-flow: column nowrap;
height: 250px;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
&__front {
transform: rotateY(0deg);
}
&__back {
transform: rotateY(180deg);
}
}
.ng-invalid {
&.ng-touched {
border-color: $card-font-invalid-colour;
}
}
.ng-valid {
&.ng-touched {
border-color: $input-border-color;
}
}