File

projects/ng-payment-card/src/lib/payment-card.component.ts

Description

NgPaymentCard without any dependencies other then ReactiveFormsModule

Implements

OnInit

Metadata

encapsulation ViewEncapsulation.None
selector ng-payment-card
styleUrls ./payment-card.component.scss
templateUrl ./payment-card.component.html

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(_ccService: PaymentCardService, _fb: FormBuilder)
Parameters :
Name Type Optional
_ccService PaymentCardService No
_fb FormBuilder No

Inputs

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

Outputs

formSaved
Type : EventEmitter<ICardDetails>

EventEmitter for payment card object

Methods

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 :
Name Type Optional
ccNum string No
Returns : string | null
Public ngOnInit
ngOnInit()
Returns : void

Properties

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">&nbsp;</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;
  }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""