├── projects ├── ngx-interactive-paycard-demo │ ├── src │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── chip.png │ │ │ ├── logo.png │ │ │ ├── SplitShire1.jpg │ │ │ ├── SplitShire3.jpg │ │ │ └── chip_backup.png │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── app.module.ts │ │ │ ├── app.component.html │ │ │ └── app.component.ts │ │ ├── styles.scss │ │ ├── index.html │ │ ├── main.ts │ │ ├── test.ts │ │ └── polyfills.ts │ ├── e2e │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── app.po.ts │ │ │ └── app.e2e-spec.ts │ │ └── protractor.conf.js │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── tsconfig.app.json │ ├── browserslist │ └── karma.conf.js └── ngx-interactive-paycard-lib │ ├── src │ ├── assets │ │ └── style.scss │ ├── lib │ │ ├── shared │ │ │ ├── index.ts │ │ │ ├── focused-element.ts │ │ │ ├── card-label-model.ts │ │ │ ├── card-model.ts │ │ │ ├── form-label-model.ts │ │ │ ├── default-component-labels.ts │ │ │ ├── if-every-changes.directive.ts │ │ │ ├── if-undefined-changes.directive.ts │ │ │ ├── if-every-changes.directive.spec.ts │ │ │ └── if-undefined.changes.directive.spec.ts │ │ ├── interactive-paycard.module.ts │ │ ├── interactive-paycard.component.html │ │ ├── card │ │ │ ├── card.component.spec.ts │ │ │ ├── card.component.html │ │ │ ├── card.component.ts │ │ │ └── card.component.scss │ │ ├── interactive-paycard.component.scss │ │ ├── interactive-paycard.component.ts │ │ └── interactive-paycard.component.spec.ts │ ├── public-api.ts │ └── test.ts │ ├── tslint.json │ ├── ng-package.json │ ├── package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── karma.conf.js │ └── README.md ├── paycard-demo.gif ├── .editorconfig ├── .travis.yml ├── tsconfig.base.json ├── .vscode └── launch.json ├── tsconfig.json ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── tslint.json ├── package.json ├── README.md └── angular.json /projects/ngx-interactive-paycard-demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/assets/style.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json" 3 | } 4 | -------------------------------------------------------------------------------- /paycard-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/paycard-demo.gif -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card-label-model'; 2 | export * from './form-label-model'; 3 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/assets/chip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/assets/chip.png -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/assets/logo.png -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/focused-element.ts: -------------------------------------------------------------------------------- 1 | export enum FocusedElement { 2 | CardNumber, 3 | CardName, 4 | ExpirationDate, 5 | CVV 6 | } 7 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/assets/SplitShire1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/assets/SplitShire1.jpg -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/assets/SplitShire3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/assets/SplitShire3.jpg -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/assets/chip_backup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milantenk/ngx-interactive-paycard/HEAD/projects/ngx-interactive-paycard-demo/src/assets/chip_backup.png -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | min-height: 80vh; 3 | display: flex; 4 | padding: 50px 15px; 5 | flex-wrap: wrap; 6 | flex-direction: column; 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/card-label-model.ts: -------------------------------------------------------------------------------- 1 | export interface CardLabel { 2 | expires: string; 3 | cardHolder: string; 4 | fullName: string; 5 | mm: string; 6 | yy: string; 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/card-model.ts: -------------------------------------------------------------------------------- 1 | export class CardModel { 2 | cardNumber: string; 3 | cardName: string; 4 | expirationMonth: string; 5 | expirationYear: string; 6 | cvv: string; 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-interactive-paycard-lib", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-interactive-paycard", 3 | "version": "2.0.1", 4 | "license": "MIT", 5 | "peerDependencies": { 6 | "@angular/common": "^10.0.6", 7 | "@angular/core": "^10.0.6" 8 | } 9 | } -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "angularCompilerOptions": { 5 | "enableIvy": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ngx-interactive-paycard-lib 3 | */ 4 | 5 | export * from './lib/interactive-paycard.component'; 6 | export * from './lib/interactive-paycard.module'; 7 | export * from './lib/shared'; 8 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/form-label-model.ts: -------------------------------------------------------------------------------- 1 | export interface FormLabel { 2 | cardNumber: string; 3 | cardHolderName: string; 4 | expirationDate: string; 5 | expirationMonth: string; 6 | expirationYear: string; 7 | cvv: string; 8 | submitButton: string; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | body { 3 | background: #ddeefc; 4 | font-family: "Source Sans Pro", sans-serif; 5 | font-size: 16px; 6 | } 7 | * { 8 | box-sizing: border-box; 9 | &:focus { 10 | outline: none; 11 | } 12 | } -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InteractivePaycardDemo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { InteractivePaycardModule } from 'ngx-interactive-paycard-lib'; 6 | 7 | @NgModule({ 8 | declarations: [ 9 | AppComponent 10 | ], 11 | imports: [ 12 | BrowserModule, 13 | InteractivePaycardModule 14 | ], 15 | providers: [], 16 | bootstrap: [AppComponent] 17 | }) 18 | export class AppModule { } 19 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/default-component-labels.ts: -------------------------------------------------------------------------------- 1 | export enum DefaultComponentLabels { 2 | FORM_CARD_NUMBER = 'Card Number', 3 | FORM_CARD_HOLDER_NAME = 'Card Holder Name', 4 | FORM_EXPIRATION_DATE = 'Expiration Date', 5 | FORM_EXPIRATION_MONTH = 'Month', 6 | FORM_EXPIRATION_YEAR = 'Year', 7 | FORM_CVV = 'CVV', 8 | FORM_SUBMIT_BUTTON = 'Submit', 9 | CARD_EXPIRES = 'Expires', 10 | CARD_HOLDER_NAME = 'Card Holder', 11 | CARD_FULL_NAME = 'Full Name', 12 | CARD_EXPIRATION_YEAR_FORMAT = 'YY', 13 | CARD_EXPIRATION_MONTH_FORMAT = 'MM' 14 | } 15 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getInputElementsNumber() { 9 | return element.all(by.css('input')); 10 | } 11 | 12 | getSelectElementsNumber() { 13 | return element.all(by.css('select')); 14 | } 15 | 16 | getButtonText() { 17 | return element(by.buttonText('Enviar')).getText() as Promise; 18 | } 19 | 20 | getElementById(id: string) { 21 | return element(by.id(id)); 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | addons: 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | before_install: 11 | - pip install --user codecov 12 | after_success: 13 | - codecov --file coverage/ngx-interactive-paycard-lib/lcov.info --disable search 14 | script: 15 | - npm run lint:lib 16 | - npm run test:lib:ci 17 | - npm run e2e:demo 18 | before_deploy: 19 | - npm run build:lib:prod 20 | - cd dist/ngx-interactive-paycard-lib 21 | deploy: 22 | provider: npm 23 | email: "$NPM_EMAIL" 24 | api:key: "$NPM_API_TOKEN" 25 | skip_cleanup: true 26 | on: 27 | tags: true -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "target": "es2015", 7 | "declaration": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": [ 11 | "dom", 12 | "es2018" 13 | ] 14 | }, 15 | "angularCompilerOptions": { 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "enableResourceInlining": true 19 | }, 20 | "exclude": [ 21 | "src/test.ts", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "target": "es2015", 13 | "module": "es2020", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "paths": { 19 | "ngx-interactive-paycard-lib": [ 20 | "dist/ngx-interactive-paycard-lib" 21 | ], 22 | "ngx-interactive-paycard-lib/*": [ 23 | "dist/ngx-interactive-paycard-lib/*" 24 | ] 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:4200", 12 | "webRoot": "${workspaceFolder}", 13 | "sourceMapPathOverrides": { 14 | "webpack:///ng://ngx-interactive-paycard/lib/*": "${workspaceFolder}/projects/ngx-interactive-paycard-lib/src/lib/*" 15 | }, 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./projects/ngx-interactive-paycard-lib/tsconfig.lib.json" 12 | }, 13 | { 14 | "path": "./projects/ngx-interactive-paycard-lib/tsconfig.spec.json" 15 | }, 16 | { 17 | "path": "./projects/ngx-interactive-paycard-demo/tsconfig.app.json" 18 | }, 19 | { 20 | "path": "./projects/ngx-interactive-paycard-demo/tsconfig.spec.json" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | import 'zone.js/dist/zone'; 9 | import 'zone.js/dist/zone-testing'; 10 | 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting() 17 | ); 18 | // Then we find all the tests. 19 | const context = require.context('./', true, /\.spec\.ts$/); 20 | // And load the modules. 21 | context.keys().map(context); 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ngx-interactive-paycard 2 | All contributions are welcome! Here are some guidelines which help you to get started. 3 | 4 | ## Getting started 5 | If needed please check out one of the getting started guides about GitHub fork / pull requests workflow: 6 | 7 | * [Forking project](https://guides.github.com/activities/forking/) 8 | * [How to sync your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 9 | 10 | ## Sending pull requests 11 | Before you start to work on a topic please create an issue and let's discuss what are you going to contribute. Pull requests can be sent against the `master` branch. 12 | 13 | ## Coding Guidelines 14 | In the project follows the [styleguide of Angular](https://angular.io/guide/styleguide). 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/interactive-paycard.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | import { CardComponent } from './card/card.component'; 8 | import { InteractivePaycardComponent } from './interactive-paycard.component'; 9 | import { IfEveryChangesDirective } from './shared/if-every-changes.directive'; 10 | import { IfUndefinedChangesDirective } from './shared/if-undefined-changes.directive'; 11 | 12 | @NgModule({ 13 | declarations: [InteractivePaycardComponent, CardComponent, IfUndefinedChangesDirective, IfEveryChangesDirective], 14 | imports: [ 15 | FormsModule, 16 | CommonModule, 17 | BrowserAnimationsModule 18 | ], 19 | exports: [InteractivePaycardComponent] 20 | }) 21 | export class InteractivePaycardModule { } 22 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome', 17 | chromeOptions: { 18 | args: ['--headless', '--no-sandbox'] 19 | } 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:4200/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 34 | } 35 | }; -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/if-every-changes.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; 2 | 3 | // Used by animations, see following issue for expanition: 4 | // https://github.com/angular/angular/issues/29439 5 | @Directive({ 6 | selector: '[ifEveryChanges]' 7 | }) 8 | export class IfEveryChangesDirective { 9 | private currentValue: any; 10 | private hasView = false; 11 | 12 | constructor( 13 | private viewContainer: ViewContainerRef, 14 | private templateRef: TemplateRef 15 | ) { } 16 | 17 | @Input() set ifEveryChanges(val: any) { 18 | if (!this.hasView) { 19 | this.viewContainer.createEmbeddedView(this.templateRef); 20 | this.hasView = true; 21 | this.currentValue = val; 22 | } else if (val !== this.currentValue) { 23 | this.viewContainer.clear(); 24 | this.viewContainer.createEmbeddedView(this.templateRef); 25 | this.currentValue = val; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display 3 input elements', () => { 12 | page.navigateTo(); 13 | expect(page.getInputElementsNumber().count()).toEqual(3); 14 | }) 15 | 16 | it('should display 2 select elements', () => { 17 | page.navigateTo(); 18 | expect(page.getSelectElementsNumber().count()).toEqual(2); 19 | }) 20 | 21 | it('should check if the submit button exists', () => { 22 | page.navigateTo(); 23 | expect(page.getButtonText()).toBeDefined(); 24 | }); 25 | 26 | afterEach(async () => { 27 | // Assert that there are no errors emitted from the browser 28 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 29 | expect(logs).not.toContain(jasmine.objectContaining({ 30 | level: logging.Level.SEVERE, 31 | } as logging.Entry)); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/if-undefined-changes.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; 2 | 3 | // Used by animations, see following issue for expanition: 4 | // https://github.com/angular/angular/issues/29439 5 | @Directive({ 6 | selector: '[ifUndefinedChanges]' 7 | }) 8 | export class IfUndefinedChangesDirective { 9 | private currentValue: any; 10 | private hasView = false; 11 | 12 | constructor( 13 | private viewContainer: ViewContainerRef, 14 | private templateRef: TemplateRef 15 | ) { } 16 | 17 | @Input() set ifUndefinedChanges(val: any) { 18 | if (!this.hasView) { 19 | this.viewContainer.createEmbeddedView(this.templateRef); 20 | this.hasView = true; 21 | this.currentValue = val; 22 | } else if (val !== this.currentValue && (!this.currentValue || !val)) { 23 | this.viewContainer.clear(); 24 | this.viewContainer.createEmbeddedView(this.templateRef); 25 | this.currentValue = val; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/ngx-interactive-paycard-demo'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/ngx-interactive-paycard-lib'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | customLaunchers: { 25 | ChromeHeadlessNoSandbox: { 26 | base: 'ChromeHeadless', 27 | flags: ['--no-sandbox'] 28 | } 29 | }, 30 | port: 9876, 31 | colors: true, 32 | logLevel: config.LOG_INFO, 33 | autoWatch: true, 34 | browsers: ['Chrome'], 35 | singleRun: true, 36 | restartOnFileChange: true 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CardLabel, FormLabel } from 'ngx-interactive-paycard-lib'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent { 10 | title = 'ngx-interactive-paycard-demo'; 11 | cardNumberFormat = '#### #### #### ####'; 12 | cardNumberMask = '#### **** **** ####'; 13 | // cardLabel change the values of the card labels. ex: Spanish 14 | cardLabel: CardLabel = { 15 | expires: 'Expira', 16 | cardHolder: 'Nombre del Titular', 17 | fullName: 'Nombre completo', 18 | mm: 'MM', 19 | yy: 'AA', 20 | }; 21 | // cardLabel change the values of the form labels. ex: Spanish 22 | formLabel: FormLabel = { 23 | cardNumber: 'Número de Tarjeta', 24 | cardHolderName: 'Titular de la Tarjeta', 25 | expirationDate: 'Fecha de Expiracion', 26 | expirationMonth: 'Mes', 27 | expirationYear: 'Año', 28 | cvv: 'CVV', 29 | submitButton: 'Enviar', 30 | }; 31 | 32 | onSubmitEvent($event) { 33 | console.log($event); 34 | } 35 | 36 | showChangesCard($event) { 37 | // any changes on card (number, name, month, year, cvv) 38 | console.log($event); 39 | } 40 | 41 | showChangesCardNumber($event) { 42 | // any changes on card number 43 | console.log($event); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/if-every-changes.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { IfEveryChangesDirective } from './if-every-changes.directive'; 2 | 3 | describe('IfEveryChangesDirective', () => { 4 | 5 | const viewContainerRefMock = { createEmbeddedView: () => { }, clear: () => { } }; 6 | 7 | beforeEach(() => { 8 | spyOn(viewContainerRefMock, 'createEmbeddedView'); 9 | spyOn(viewContainerRefMock, 'clear'); 10 | }); 11 | 12 | it('should create embedded view without clear if there is no previous value yet', () => { 13 | // Arrange 14 | const directive = new IfEveryChangesDirective(viewContainerRefMock as any, null); 15 | 16 | // Act 17 | directive.ifEveryChanges = 'test'; 18 | 19 | // Assert 20 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalled(); 21 | expect(viewContainerRefMock.clear).not.toHaveBeenCalled(); 22 | }); 23 | 24 | it('should create embedded view and update from the previous value if the value changed', () => { 25 | // Arrange 26 | const directive = new IfEveryChangesDirective(viewContainerRefMock as any, null); 27 | 28 | // Act 29 | directive.ifEveryChanges = 'test'; 30 | directive.ifEveryChanges = 'test2'; 31 | 32 | // Assert 33 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalledTimes(2); 34 | expect(viewContainerRefMock.clear).toHaveBeenCalledTimes(1); 35 | }); 36 | 37 | it('should create embedded view and not update if the value does not change', () => { 38 | // Arrange 39 | const directive = new IfEveryChangesDirective(viewContainerRefMock as any, null); 40 | 41 | // Act 42 | directive.ifEveryChanges = 'test'; 43 | directive.ifEveryChanges = 'test2'; 44 | directive.ifEveryChanges = 'test2'; 45 | directive.ifEveryChanges = 'test2'; 46 | 47 | // Assert 48 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalledTimes(2); 49 | expect(viewContainerRefMock.clear).toHaveBeenCalledTimes(1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/shared/if-undefined.changes.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { IfUndefinedChangesDirective } from './if-undefined-changes.directive'; 2 | 3 | describe('IfUndefinedChangesDirective', () => { 4 | 5 | const viewContainerRefMock = { createEmbeddedView: () => { }, clear: () => { } }; 6 | 7 | beforeEach(() => { 8 | spyOn(viewContainerRefMock, 'createEmbeddedView'); 9 | spyOn(viewContainerRefMock, 'clear'); 10 | }); 11 | 12 | it('should create embedded view without clear if there is no previous value yet', () => { 13 | // Arrange 14 | const directive = new IfUndefinedChangesDirective(viewContainerRefMock as any, null); 15 | 16 | // Act 17 | directive.ifUndefinedChanges = 'test'; 18 | 19 | // Assert 20 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalled(); 21 | expect(viewContainerRefMock.clear).not.toHaveBeenCalled(); 22 | }); 23 | 24 | it('should create embedded view and not update from the previous value if the previous value is not undefined', () => { 25 | // Arrange 26 | const directive = new IfUndefinedChangesDirective(viewContainerRefMock as any, null); 27 | 28 | // Act 29 | directive.ifUndefinedChanges = 'test'; 30 | directive.ifUndefinedChanges = 'test2'; 31 | 32 | // Assert 33 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalledTimes(1); 34 | expect(viewContainerRefMock.clear).not.toHaveBeenCalled(); 35 | }); 36 | 37 | it('should create embedded view and not update from the previous value if the previous value is was undefined', () => { 38 | // Arrange 39 | const directive = new IfUndefinedChangesDirective(viewContainerRefMock as any, null); 40 | 41 | // Act 42 | directive.ifUndefinedChanges = 'test'; 43 | directive.ifUndefinedChanges = undefined; 44 | directive.ifUndefinedChanges = 'test2'; 45 | 46 | // Assert 47 | expect(viewContainerRefMock.createEmbeddedView).toHaveBeenCalledTimes(3); 48 | expect(viewContainerRefMock.clear).toHaveBeenCalledTimes(2); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Milán Tenk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | License of vue-interactive-paycard: 26 | 27 | MIT License 28 | 29 | Copyright (c) 2019 Muhammed Erdem 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. 48 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warning" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-var-requires": false, 52 | "object-literal-key-quotes": [ 53 | true, 54 | "as-needed" 55 | ], 56 | "object-literal-sort-keys": false, 57 | "ordered-imports": true, 58 | "quotemark": [ 59 | true, 60 | "single" 61 | ], 62 | "trailing-comma": false, 63 | "component-class-suffix": true, 64 | "contextual-lifecycle": true, 65 | "directive-class-suffix": true, 66 | "no-conflicting-lifecycle": true, 67 | "no-host-metadata-property": true, 68 | "no-input-rename": true, 69 | "no-inputs-metadata-property": true, 70 | "no-output-native": true, 71 | "no-output-on-prefix": true, 72 | "no-output-rename": true, 73 | "no-outputs-metadata-property": true, 74 | "template-banana-in-box": true, 75 | "template-no-negated-async": true, 76 | "use-lifecycle-interface": true, 77 | "use-pipe-transform-interface": true, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type", 85 | "check-typecast" 86 | ], 87 | "import-destructuring-spacing": true, 88 | "import-spacing": true, 89 | "cyclomatic-complexity": [true, 10], 90 | "semicolon": [true, "always"] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-interactive-paycard-workspace", 3 | "version": "2.0.1", 4 | "license": "MIT", 5 | "engines": { 6 | "node": "12.16.3", 7 | "npm": "6.14.4" 8 | }, 9 | "scripts": { 10 | "ng": "ng", 11 | "clean": "rimraf dist", 12 | "e2e:demo": "npm run build:lib && ng e2e", 13 | "build:demo": "ng build ngx-interactive-paycard-demo", 14 | "build:lib": "ng build ngx-interactive-paycard-lib", 15 | "build:lib:prod": "ng build ngx-interactive-paycard-lib --prod", 16 | "build:all": "npm run build:lib && npm run build:demo -- --prod", 17 | "start:demo": "wait-on dist/ngx-interactive-paycard-lib/fesm2015 && ng serve --poll 2000", 18 | "watch:lib": "ng build ngx-interactive-paycard-lib --watch", 19 | "watch:all": "npm run clean && run-p watch:lib start:demo", 20 | "test:lib": "ng test ngx-interactive-paycard-lib", 21 | "test:lib:watch": "ng test ngx-interactive-paycard-lib --watch=true", 22 | "test:lib:cc": "ng test ngx-interactive-paycard-lib --codeCoverage=true", 23 | "test:lib:ci": "ng test ngx-interactive-paycard-lib --no-progress --browsers=ChromeHeadlessNoSandbox --codeCoverage=true", 24 | "lint:lib": "ng lint ngx-interactive-paycard-lib", 25 | "lint:lib:fix": "ng lint ngx-interactive-paycard-lib --fix" 26 | }, 27 | "private": true, 28 | "dependencies": { 29 | "@angular/animations": "~10.0.6", 30 | "@angular/common": "~10.0.6", 31 | "@angular/compiler": "~10.0.6", 32 | "@angular/core": "~10.0.6", 33 | "@angular/forms": "~10.0.6", 34 | "@angular/platform-browser": "~10.0.6", 35 | "@angular/platform-browser-dynamic": "~10.0.6", 36 | "@angular/router": "~10.0.6", 37 | "rxjs": "~6.5.5", 38 | "tslib": "^2.0.0", 39 | "zone.js": "~0.10.3" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~0.1000.5", 43 | "@angular-devkit/build-ng-packagr": "~0.1000.5", 44 | "@angular/cli": "~10.0.5", 45 | "@angular/compiler-cli": "~10.0.6", 46 | "@types/node": "^12.11.1", 47 | "@types/jasmine": "~3.5.0", 48 | "@types/jasminewd2": "~2.0.3", 49 | "codelyzer": "^6.0.0", 50 | "jasmine-core": "~3.5.0", 51 | "jasmine-spec-reporter": "~5.0.0", 52 | "karma": "~5.0.0", 53 | "karma-chrome-launcher": "~3.1.0", 54 | "karma-coverage-istanbul-reporter": "~3.0.2", 55 | "karma-jasmine": "~3.3.0", 56 | "karma-jasmine-html-reporter": "^1.5.0", 57 | "ng-packagr": "^10.0.0", 58 | "protractor": "~7.0.0", 59 | "ts-node": "~8.3.0", 60 | "tslint": "~6.1.0", 61 | "typescript": "~3.9.5", 62 | "wait-on": "^3.3.0", 63 | "rimraf": "^3.0.0", 64 | "npm-run-all": "^4.1.5" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/interactive-paycard.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 |
8 |
9 | 10 | 13 |
14 |
15 | 16 | 18 |
19 |
20 |
21 |
22 | 23 | 31 | 38 |
39 |
40 |
41 |
42 | 43 | 45 |
46 |
47 |
48 | 49 |
50 |
51 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/README.md: -------------------------------------------------------------------------------- 1 | # ngx-interactive-paycard 2 | 3 | A parameterizable animated credit card built with Angular. 4 | [See live demo here.](https://ngx-interactive-paycard.netlify.app/) 5 | 6 | # Using the library 7 | The library is published in Angular package format. To install the library run in the consumer project following command: 8 | 9 | ```bash 10 | npm install ngx-interactive-paycard 11 | ``` 12 | 13 | Import the module of the paycard: 14 | 15 | ```javascript 16 | import { InteractivePaycardModule } from 'ngx-interactive-paycard'; 17 | 18 | @NgModule({ 19 | ... 20 | imports: [ 21 | ... 22 | InteractivePaycardModule, 23 | ... 24 | ], 25 | ... 26 | }) 27 | export class UsedModule { } 28 | ``` 29 | 30 | To embed the card use the `` selector. 31 | 32 | It has following input parameters: 33 | * `chipImgPath`: The path of the image which should be displayed as chip on the card. 34 | * `logoImagePath`: The path of the company logo image. 35 | * `frontBgImagePath`: The path of the card front background image. 36 | * `backBgImagePath`: The path of the card back background image. 37 | * `cardNumberFormat`: The format of the card number specified with `#` charaters.
For example `"#### #### #### ####"` is a pattern for Master or VISA cards. 38 | * `cardNumberMask`: Specifies which part of the card number should be masked. The masked characters are defined using `*` character the unmasked numbers are defined with `#` character. For example `"#### **** **** ####"` masks the middle of the card number. Note that it should have the same number of characters as the `cardNumberFormat` has. 39 | * `cardLabels`: Optional property to modify all labels in the card component. 40 | * `formLabels`: Optional property to modify all labels in form component. 41 | 42 | The output parameters are following: 43 | * `submitEvent`: It is fired if the Submit button is clicked. The event contains all the card data. 44 | * `changeCard`: It is fired if one of the card properties change. The event contains all the card data. 45 | * `changeCardNumber`: It is fired if the card number changes. The event contains the card number. 46 | 47 | An example for the usage can be found below. The example assumes, that the consumer `assets` folder contains the necessary images. 48 | 49 | ```html 50 | 63 | 64 | ``` 65 | 66 | And the component code for it: 67 | 68 | ```javascript 69 | @Component({ 70 | selector: 'app-root', 71 | templateUrl: './app.component.html', 72 | styleUrls: ['./app.component.scss'] 73 | }) 74 | export class AppComponent { 75 | title = 'ngx-interactive-paycard-demo'; 76 | cardNumberFormat = "#### #### #### ####"; 77 | cardNumberMask = "#### **** **** ####"; 78 | //ex: Optional cardLabels - Spanish 79 | cardLabel: CardLabel = { 80 | expires: 'Expira', 81 | cardHolder: 'Nombre del Titular', 82 | fullName: 'Nombre completo', 83 | mm: 'MM', 84 | yy: 'AA', 85 | }; 86 | //ex: Optional formLabels - Spanish 87 | formLabel: FormLabel = { 88 | cardNumber: 'Número de Tarjeta', 89 | cardHolderName: 'Titular de la Tarjeta', 90 | expirationDate: 'Fecha de Expiracion', 91 | expirationMonth: 'Mes', 92 | expirationYear: 'Año', 93 | cvv: 'CVV', 94 | submitButton: 'Enviar', 95 | }; 96 | 97 | onSubmitEvent($event) { 98 | console.log($event); 99 | } 100 | 101 | showChangesCard($event) { 102 | // any changes on card (number, name, month, year, cvv) 103 | console.log($event); 104 | } 105 | 106 | showChangesCardNumber($event) { 107 | // any changes on card number 108 | console.log($event); 109 | } 110 | } 111 | ``` 112 | 113 | A working example can be found in the `ngx-interactive-paycard-demo` folder in the repository of the library. 114 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/card/card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { FocusedElement } from '../shared/focused-element'; 2 | import { CardComponent } from './card.component'; 3 | 4 | describe('CardComponent', () => { 5 | let component: any; 6 | 7 | const offsetWidth = 'offsetWidth'; 8 | const offsetHeight = 'offsetHeight'; 9 | const offsetLeft = 'offsetWidth'; 10 | const offsetTop = 'offsetTop'; 11 | 12 | beforeEach(() => { 13 | component = new CardComponent(); 14 | }); 15 | 16 | beforeEach(() => { 17 | component.currentlyFocusedNativeElement = { 18 | offsetWidth, 19 | offsetHeight, 20 | offsetLeft, 21 | offsetTop 22 | }; 23 | }); 24 | 25 | describe('#onOrientationChange', () => { 26 | const delay = 50; 27 | beforeEach(() => { 28 | component.setFocusStyle = jasmine.createSpy('setFocusStyle'); 29 | }); 30 | 31 | it('should do nothing if has no focus native element', (done: any) => { 32 | component.currentlyFocusedNativeElement = null; 33 | component.onOrientationChange(); 34 | setTimeout(() => { 35 | expect(component.setFocusStyle).not.toHaveBeenCalled(); 36 | done(); 37 | }, delay); 38 | }); 39 | 40 | it('should set focus styles after delay if native element exists', (done: any) => { 41 | component.onOrientationChange(); 42 | expect(component.setFocusStyle).not.toHaveBeenCalled(); 43 | setTimeout(() => { 44 | expect(component.setFocusStyle).toHaveBeenCalled(); 45 | done(); 46 | }, delay); 47 | }); 48 | }); 49 | 50 | describe('#setFocusStyle', () => { 51 | it('should set focus style from current focused native element', () => { 52 | component.setFocusStyle(); 53 | expect(component.focusStyle).toEqual({ 54 | width: `${offsetWidth}px`, 55 | height: `${offsetHeight}px`, 56 | transition: 'none', 57 | transform: jasmine.any(String) 58 | }); 59 | expect(component.focusStyle.transform).toContain(`translateX(${offsetLeft}px)`); 60 | expect(component.focusStyle.transform).toContain(`translateY(${offsetTop}px)`); 61 | }); 62 | }); 63 | 64 | describe('#ngOnChanges', () => { 65 | const cardNumberViewChildNativeElement = 'cardNumberViewChildNativeElement'; 66 | const cardNameViewChildNativeElement = 'cardNameViewChildNativeElement'; 67 | const expireDateViewChildNativeElement = 'expireDateViewChildNativeElement'; 68 | 69 | beforeEach(() => { 70 | component.cardNumberViewChild = { nativeElement: cardNumberViewChildNativeElement }; 71 | component.cardNameViewChild = { nativeElement: cardNameViewChildNativeElement }; 72 | component.expireDateViewChild = { nativeElement: expireDateViewChildNativeElement }; 73 | }); 74 | 75 | it('should set native element based on current focused element', () => { 76 | component.ngOnChanges({focusedElement: {currentValue: FocusedElement.CardNumber}}); 77 | expect(component.currentlyFocusedNativeElement).toBe(cardNumberViewChildNativeElement); 78 | 79 | component.ngOnChanges({focusedElement: {currentValue: FocusedElement.CardName}}); 80 | expect(component.currentlyFocusedNativeElement).toBe(cardNameViewChildNativeElement); 81 | 82 | component.ngOnChanges({focusedElement: {currentValue: FocusedElement.ExpirationDate}}); 83 | expect(component.currentlyFocusedNativeElement).toBe(expireDateViewChildNativeElement); 84 | }); 85 | 86 | it('should set focus style based on current focused native element if it is defined', () => { 87 | component.cardNumberViewChild = { nativeElement: component.currentlyFocusedNativeElement }; 88 | component.ngOnChanges({focusedElement: {currentValue: FocusedElement.CardNumber}}); 89 | expect(component.focusStyle).toEqual({ 90 | width: `${offsetWidth}px`, 91 | height: `${offsetHeight}px`, 92 | transform: jasmine.any(String) 93 | }); 94 | expect(component.focusStyle.transform).toContain(`translateX(${offsetLeft}px)`); 95 | expect(component.focusStyle.transform).toContain(`translateY(${offsetTop}px)`); 96 | }); 97 | }); 98 | }); -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/card/card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 | 30 |
31 | 42 |
43 | 44 | 50 | / 51 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 |
CVV
68 |
69 | * 70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, state, style, transition, trigger } from '@angular/animations'; 2 | import { Component, ElementRef, HostListener, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; 3 | 4 | import { CardLabel } from '../shared'; 5 | import { CardModel } from '../shared/card-model'; 6 | import { FocusedElement } from '../shared/focused-element'; 7 | 8 | @Component({ 9 | selector: 'card', 10 | templateUrl: 'card.component.html', 11 | styleUrls: ['card.component.scss'], 12 | animations: [ 13 | trigger('slideFadeUp', [ 14 | state('in', style({ transform: 'translateY(0)' })), 15 | transition(':enter', [ 16 | style({ transform: 'translateY(15px)', opacity: 0 }), 17 | animate('0.25s ease-in-out') 18 | ]) 19 | ]), 20 | trigger('cardHolderFadeRight', [ 21 | state('in', style({ transform: 'translate(0,0)' })), 22 | transition(':enter', [ 23 | style({ transform: 'translateX(10px) rotate(45deg)', opacity: 0, position: 'absolute' }), 24 | animate('0.25s ease-in-out') 25 | ]), 26 | ]), 27 | trigger('cardHolderFadeUp', [ 28 | state('in', style({ transform: 'translate(0,0)' })), 29 | transition(':enter', [ 30 | style({ transform: 'translateY(15px)', opacity: 0, position: 'absolute' }), 31 | animate('0.25s ease-in-out') 32 | ]), 33 | ]) 34 | ] 35 | }) 36 | export class CardComponent implements OnInit, OnChanges { 37 | constructor() { } 38 | 39 | @Input() cardModel: CardModel; 40 | @Input() chipImgPath: string; 41 | @Input() logoImgPath: string; 42 | @Input() backBgImgPath: string; 43 | @Input() frontBgImgPath: string; 44 | @Input() cardNumberFormat: string; 45 | @Input() displayedCardNumber: string; 46 | @Input() focusedElement: FocusedElement; 47 | @Input() cardLabels: CardLabel; 48 | 49 | @ViewChild('cardNumber', { static: false }) cardNumberViewChild: ElementRef; 50 | @ViewChild('cardName', { static: false }) cardNameViewChild: ElementRef; 51 | @ViewChild('expireDate', { static: false }) expireDateViewChild: ElementRef; 52 | 53 | currentCardNumberPlaceholder: string[]; 54 | cardHolderNamePlaceholder: string[]; 55 | focusStyle = null; 56 | 57 | FocusedElement = FocusedElement; // This way the enum can be accessed in the template 58 | 59 | currentlyFocusedNativeElement: any; 60 | 61 | @HostListener('window:orientationchange', ['$event']) 62 | onOrientationChange() { 63 | if (this.currentlyFocusedNativeElement) { 64 | setTimeout(this.setFocusStyle, 50); // Workaround: if the orientation changes, the ViewChild won't be updated immediatelly 65 | } 66 | } 67 | 68 | ngOnInit() { 69 | this.currentCardNumberPlaceholder = this.cardNumberFormat.split(''); 70 | this.cardHolderNamePlaceholder = Array(30).fill(''); // CardHolder name is handled the same way as the cardNumber 71 | } 72 | 73 | getIsNumberMasked(index: number): boolean { 74 | return this.displayedCardNumber[index] === '*'; 75 | } 76 | 77 | // The selection of the card elements is handled here 78 | // The card flip based on the CVV is handled in the template directly 79 | ngOnChanges(changes: SimpleChanges) { 80 | for (const propName in changes) { 81 | if (propName === 'focusedElement') { 82 | if (changes[propName].currentValue != null) { 83 | // let focusedNativeElement; 84 | if (changes[propName].currentValue === FocusedElement.CardNumber) { 85 | this.currentlyFocusedNativeElement = this.cardNumberViewChild.nativeElement; 86 | } else if (changes[propName].currentValue === FocusedElement.CardName) { 87 | this.currentlyFocusedNativeElement = this.cardNameViewChild.nativeElement; 88 | } else if (changes[propName].currentValue === FocusedElement.ExpirationDate) { 89 | this.currentlyFocusedNativeElement = this.expireDateViewChild.nativeElement; 90 | } 91 | if (this.currentlyFocusedNativeElement) { 92 | this.focusStyle = { 93 | width: `${this.currentlyFocusedNativeElement.offsetWidth}px`, 94 | height: `${this.currentlyFocusedNativeElement.offsetHeight}px`, 95 | transform: `translateX(${this.currentlyFocusedNativeElement.offsetLeft}px) translateY(${this.currentlyFocusedNativeElement.offsetTop}px)` 96 | }; 97 | } 98 | } else { 99 | this.focusStyle = null; 100 | } 101 | } 102 | } 103 | } 104 | 105 | private setFocusStyle = () => { 106 | this.focusStyle = { 107 | width: `${this.currentlyFocusedNativeElement.offsetWidth}px`, 108 | height: `${this.currentlyFocusedNativeElement.offsetHeight}px`, 109 | transition: 'none', 110 | transform: `translateX(${this.currentlyFocusedNativeElement.offsetLeft}px) 111 | translateY(${this.currentlyFocusedNativeElement.offsetTop}px)` 112 | }; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/interactive-paycard.component.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | &:focus { 4 | outline: none; 5 | } 6 | } 7 | .wrapper { 8 | min-height: 100vh; 9 | display: flex; 10 | padding: 50px 15px; 11 | @media screen and (max-width: 700px), (max-height: 500px) { 12 | flex-wrap: wrap; 13 | flex-direction: column; 14 | } 15 | } 16 | 17 | .card-form { 18 | max-width: 570px; 19 | margin: auto; 20 | width: 100%; 21 | 22 | @media screen and (max-width: 576px) { 23 | margin: 0 auto; 24 | } 25 | 26 | &__inner { 27 | background: #fff; 28 | box-shadow: 0 30px 60px 0 rgba(90, 116, 148, 0.4); 29 | border-radius: 10px; 30 | padding: 35px; 31 | padding-top: 180px; 32 | 33 | @media screen and (max-width: 480px) { 34 | padding: 25px; 35 | padding-top: 165px; 36 | } 37 | @media screen and (max-width: 360px) { 38 | padding: 15px; 39 | padding-top: 165px; 40 | } 41 | } 42 | 43 | &__row { 44 | display: flex; 45 | align-items: flex-start; 46 | @media screen and (max-width: 480px) { 47 | flex-wrap: wrap; 48 | } 49 | } 50 | 51 | &__col { 52 | flex: auto; 53 | margin-right: 35px; 54 | 55 | &:last-child { 56 | margin-right: 0; 57 | } 58 | 59 | @media screen and (max-width: 480px) { 60 | margin-right: 0; 61 | flex: unset; 62 | width: 100%; 63 | margin-bottom: 20px; 64 | 65 | &:last-child { 66 | margin-bottom: 0; 67 | } 68 | } 69 | 70 | &.-cvv { 71 | max-width: 150px; 72 | @media screen and (max-width: 480px) { 73 | max-width: initial; 74 | } 75 | } 76 | } 77 | 78 | &__group { 79 | display: flex; 80 | align-items: flex-start; 81 | flex-wrap: wrap; 82 | 83 | .card-input__input { 84 | flex: 1; 85 | margin-right: 15px; 86 | 87 | &:last-child { 88 | margin-right: 0; 89 | } 90 | } 91 | } 92 | 93 | &__button { 94 | width: 100%; 95 | height: 55px; 96 | background: #2364d2; 97 | border: none; 98 | border-radius: 5px; 99 | font-size: 22px; 100 | font-weight: 500; 101 | font-family: "Source Sans Pro", sans-serif; 102 | box-shadow: 3px 10px 20px 0px rgba(35, 100, 210, 0.3); 103 | color: #fff; 104 | margin-top: 20px; 105 | cursor: pointer; 106 | 107 | @media screen and (max-width: 480px) { 108 | margin-top: 10px; 109 | } 110 | } 111 | } 112 | 113 | .card-list { 114 | margin-bottom: -130px; 115 | 116 | @media screen and (max-width: 480px) { 117 | margin-bottom: -120px; 118 | } 119 | } 120 | 121 | .card-input { 122 | margin-bottom: 20px; 123 | position: relative; 124 | &__label { 125 | font-size: 14px; 126 | margin-bottom: 5px; 127 | font-weight: 500; 128 | color: #1a3b5d; 129 | width: 100%; 130 | display: block; 131 | user-select: none; 132 | } 133 | &__input { 134 | width: 100%; 135 | height: 50px; 136 | border-radius: 5px; 137 | box-shadow: none; 138 | border: 1px solid #ced6e0; 139 | transition: all 0.3s ease-in-out; 140 | font-size: 18px; 141 | padding: 5px 15px; 142 | background: none; 143 | color: #1a3b5d; 144 | font-family: "Source Sans Pro", sans-serif; 145 | 146 | &:hover, 147 | &:focus { 148 | border-color: #3d9cff; 149 | } 150 | 151 | &:focus { 152 | box-shadow: 0px 10px 20px -13px rgba(32, 56, 117, 0.35); 153 | } 154 | &.-select { 155 | -webkit-appearance: none; 156 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAeCAYAAABuUU38AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAUxJREFUeNrM1sEJwkAQBdCsngXPHsQO9O5FS7AAMVYgdqAd2IGCDWgFnryLFQiCZ8EGnJUNimiyM/tnk4HNEAg/8y6ZmMRVqz9eUJvRaSbvutCZ347bXVJy/ZnvTmdJ862Me+hAbZCTs6GHpyUi1tTSvPnqTpoWZPUa7W7ncT3vK4h4zVejy8QzM3WhVUO8ykI6jOxoGA4ig3BLHcNFSCGqGAkig2yqgpEiMsjSfY9LxYQg7L6r0X6wS29YJiYQYecemY+wHrXD1+bklGhpAhBDeu/JfIVGxaAQ9sb8CI+CQSJ+QmJg0Ii/EE2MBiIXooHRQhRCkBhNhBcEhLkwf05ZCG8ICCOpk0MULmvDSY2M8UawIRExLIQIEgHDRoghihgRIgiigBEjgiFATBACAgFgghEwSAAGgoBCBBgYAg5hYKAIFYgHBo6w9RRgAFfy160QuV8NAAAAAElFTkSuQmCC"); 157 | background-size: 12px; 158 | background-position: 90% center; 159 | background-repeat: no-repeat; 160 | padding-right: 30px; 161 | } 162 | } 163 | &__eye { 164 | display: inline-flex; 165 | position: absolute; 166 | width: 1em; 167 | height: 1em; 168 | font-size: 24px; 169 | border-radius: 50%; 170 | top: 35px; 171 | right: 10px; 172 | opacity: 0.75; 173 | color: #8c9cae; 174 | cursor: pointer; 175 | padding: 0; 176 | background: none; 177 | display: inline-flex; 178 | border: 2px solid currentColor; 179 | box-shadow: none; 180 | transition: all 0.3s ease-in-out; 181 | 182 | &:before { 183 | content: ""; 184 | position: absolute; 185 | background: white; 186 | width: 0.35em; 187 | height: 0.35em; 188 | top: 6px; 189 | left: 6px; 190 | z-index: 2; 191 | border-radius: 50%; 192 | transform: scale(0.1); 193 | opacity: 0; 194 | transition: all 0.3s ease-in-out; 195 | transition-delay: 0.1s; 196 | } 197 | 198 | &:after { 199 | content: ""; 200 | position: absolute; 201 | top: 3px; 202 | left: 3px; 203 | background: currentColor; 204 | width: 0.6em; 205 | height: 0.6em; 206 | border-radius: 50%; 207 | transform: scale(0.1); 208 | opacity: 0; 209 | transition: all 0.3s ease-in-out; 210 | } 211 | 212 | &:hover:not(:disabled), 213 | &.-active:not(:disabled) { 214 | color: #2364d2; 215 | opacity: 1; 216 | } 217 | 218 | &.-active { 219 | &::before, 220 | &::after { 221 | transform: scale(1); 222 | opacity: 1; 223 | } 224 | } 225 | 226 | &:disabled { 227 | cursor: not-allowed; 228 | opacity: 0.4; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx-interactive-paycard 2 | 3 | A parameterizable animated payment card built with Angular. 4 | [See live demo here.](https://ngx-interactive-paycard.netlify.app/) 5 | 6 |

7 | Demo gif 8 |

9 | 10 | [![Build Status](https://travis-ci.org/milantenk/ngx-interactive-paycard.png?branch=master)](https://travis-ci.org/milantenk/ngx-interactive-paycard) 11 | [![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/milantenk/ngx-interactive-paycard/master.svg?style=flat)](http://codecov.io/github/milantenk/ngx-interactive-paycard?branch=master) 12 | [![Npm version](https://img.shields.io/npm/v/ngx-interactive-paycard.svg?style=flat)](https://www.npmjs.com/package/ngx-interactive-paycard) 13 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 14 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/milantenk/ngx-interactive-paycard/blob/master/CONTRIBUTING.md) 15 | 16 | # Using the library 17 | The library is published in Angular package format on in the global registry of `npmjs`. To install the library run in the consumer project following command: 18 | 19 | ```bash 20 | npm install ngx-interactive-paycard 21 | ``` 22 | 23 | Import the module of the paycard: 24 | 25 | ```javascript 26 | import { InteractivePaycardModule } from 'ngx-interactive-paycard'; 27 | 28 | @NgModule({ 29 | ... 30 | imports: [ 31 | ... 32 | InteractivePaycardModule, 33 | ... 34 | ], 35 | ... 36 | }) 37 | export class UsedModule { } 38 | ``` 39 | 40 | To embed the card use the `` selector. 41 | 42 | It has following input parameters: 43 | * `chipImgPath`: The path of the image which should be displayed as chip on the card. 44 | * `logoImagePath`: The path of the company logo image. 45 | * `frontBgImagePath`: The path of the card front background image. 46 | * `backBgImagePath`: The path of the card back background image. 47 | * `cardNumberFormat`: The format of the card number specified with `#` charaters.
For example `"#### #### #### ####"` is a pattern for Master or VISA cards. 48 | * `cardNumberMask`: Specifies which part of the card number should be masked. The masked characters are defined using `*` character the unmasked numbers are defined with `#` character. For example `"#### **** **** ####"` masks the middle of the card number. Note that it should have the same number of characters as the `cardNumberFormat` has. 49 | * `cardLabels`: Optional property to modify all labels in the card component. 50 | * `formLabels`: Optional property to modify all labels in form component. 51 | 52 | The output parameters are following: 53 | * `submitEvent`: It is fired if the Submit button is clicked. The event contains all the card data. 54 | * `changeCard`: It is fired if one of the card properties change. The event contains all the card data. 55 | * `changeCardNumber`: It is fired if the card number changes. The event contains the card number. 56 | 57 | An example for the usage can be found below. The example assumes, that the consumer `assets` folder contains the necessary images. 58 | 59 | ```html 60 | 73 | 74 | ``` 75 | 76 | And the component code for it: 77 | 78 | ```javascript 79 | @Component({ 80 | selector: 'app-root', 81 | templateUrl: './app.component.html', 82 | styleUrls: ['./app.component.scss'] 83 | }) 84 | export class AppComponent { 85 | title = 'ngx-interactive-paycard-demo'; 86 | cardNumberFormat = "#### #### #### ####"; 87 | cardNumberMask = "#### **** **** ####"; 88 | //ex: Optional cardLabels - Spanish 89 | cardLabel: CardLabel = { 90 | expires: 'Expira', 91 | cardHolder: 'Nombre del Titular', 92 | fullName: 'Nombre completo', 93 | mm: 'MM', 94 | yy: 'AA', 95 | }; 96 | //ex: Optional formLabels - Spanish 97 | formLabel: FormLabel = { 98 | cardNumber: 'Número de Tarjeta', 99 | cardHolderName: 'Titular de la Tarjeta', 100 | expirationDate: 'Fecha de Expiracion', 101 | expirationMonth: 'Mes', 102 | expirationYear: 'Año', 103 | cvv: 'CVV', 104 | submitButton: 'Enviar', 105 | }; 106 | 107 | onSubmitEvent($event) { 108 | console.log($event); 109 | } 110 | 111 | showChangesCard($event) { 112 | // any changes on card (number, name, month, year, cvv) 113 | console.log($event); 114 | } 115 | 116 | showChangesCardNumber($event) { 117 | // any changes on card number 118 | console.log($event); 119 | } 120 | } 121 | ``` 122 | 123 | A working example can be found in the `ngx-interactive-paycard-demo` folder in this repository. 124 | 125 | # Development of the library 126 | To develop the library the `LTS` version of `node.js` needs to be installed. 127 | In this repository there is an Angular workspace which contains following projects 128 | * `ng-interactive-paycard-lib`: The source code of the card library. 129 | * `ng-interactive-paycard-demo`: The consumer that is used for the library development and to showcase the features of the library. 130 | 131 | To install the dependencies of the workspace run 132 | 133 | ```bash 134 | npm install 135 | ``` 136 | 137 | To start the library and the demo project in watch mode run 138 | 139 | ```bash 140 | npm run watch:all 141 | ``` 142 | 143 | The demo of the library will be reachable on `http://localhost:4200`. 144 | 145 | # Contributing 146 | See `CONTRIBUTING.md`. 147 | 148 | # References 149 | This project is inspired by [vue-interactive-paycard](https://github.com/muhammederdem/vue-interactive-paycard). 150 | 151 | The goal of this project is to have an Angular alternative for the original vue based version. 152 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-interactive-paycard-lib": { 7 | "projectType": "library", 8 | "root": "projects/ngx-interactive-paycard-lib", 9 | "sourceRoot": "projects/ngx-interactive-paycard-lib/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-ng-packagr:build", 14 | "options": { 15 | "tsConfig": "projects/ngx-interactive-paycard-lib/tsconfig.lib.json", 16 | "project": "projects/ngx-interactive-paycard-lib/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/ngx-interactive-paycard-lib/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/ngx-interactive-paycard-lib/src/test.ts", 28 | "tsConfig": "projects/ngx-interactive-paycard-lib/tsconfig.spec.json", 29 | "karmaConfig": "projects/ngx-interactive-paycard-lib/karma.conf.js" 30 | } 31 | }, 32 | "lint": { 33 | "builder": "@angular-devkit/build-angular:tslint", 34 | "options": { 35 | "tsConfig": [ 36 | "projects/ngx-interactive-paycard-lib/tsconfig.lib.json", 37 | "projects/ngx-interactive-paycard-lib/tsconfig.spec.json" 38 | ], 39 | "exclude": [ 40 | "**/node_modules/**" 41 | ] 42 | } 43 | } 44 | } 45 | }, 46 | "ngx-interactive-paycard-demo": { 47 | "projectType": "application", 48 | "schematics": { 49 | "@schematics/angular:component": { 50 | "style": "scss" 51 | } 52 | }, 53 | "root": "projects/ngx-interactive-paycard-demo", 54 | "sourceRoot": "projects/ngx-interactive-paycard-demo/src", 55 | "prefix": "app", 56 | "architect": { 57 | "build": { 58 | "builder": "@angular-devkit/build-angular:browser", 59 | "options": { 60 | "outputPath": "dist/ngx-interactive-paycard-demo", 61 | "index": "projects/ngx-interactive-paycard-demo/src/index.html", 62 | "main": "projects/ngx-interactive-paycard-demo/src/main.ts", 63 | "polyfills": "projects/ngx-interactive-paycard-demo/src/polyfills.ts", 64 | "tsConfig": "projects/ngx-interactive-paycard-demo/tsconfig.app.json", 65 | "aot": true, 66 | "assets": [ 67 | "projects/ngx-interactive-paycard-demo/src/favicon.ico", 68 | "projects/ngx-interactive-paycard-demo/src/assets" 69 | ], 70 | "styles": [ 71 | "projects/ngx-interactive-paycard-demo/src/styles.scss" 72 | ], 73 | "scripts": [] 74 | }, 75 | "configurations": { 76 | "production": { 77 | "fileReplacements": [ 78 | { 79 | "replace": "projects/ngx-interactive-paycard-demo/src/environments/environment.ts", 80 | "with": "projects/ngx-interactive-paycard-demo/src/environments/environment.prod.ts" 81 | } 82 | ], 83 | "optimization": true, 84 | "outputHashing": "all", 85 | "sourceMap": false, 86 | "extractCss": true, 87 | "namedChunks": false, 88 | "extractLicenses": true, 89 | "vendorChunk": false, 90 | "buildOptimizer": true, 91 | "budgets": [ 92 | { 93 | "type": "initial", 94 | "maximumWarning": "2mb", 95 | "maximumError": "5mb" 96 | }, 97 | { 98 | "type": "anyComponentStyle", 99 | "maximumWarning": "6kb", 100 | "maximumError": "10kb" 101 | } 102 | ] 103 | } 104 | } 105 | }, 106 | "serve": { 107 | "builder": "@angular-devkit/build-angular:dev-server", 108 | "options": { 109 | "browserTarget": "ngx-interactive-paycard-demo:build", 110 | "sourceMap": { 111 | "scripts": true, 112 | "styles": true, 113 | "vendor": true 114 | } 115 | }, 116 | "configurations": { 117 | "production": { 118 | "browserTarget": "ngx-interactive-paycard-demo:build:production" 119 | } 120 | } 121 | }, 122 | "extract-i18n": { 123 | "builder": "@angular-devkit/build-angular:extract-i18n", 124 | "options": { 125 | "browserTarget": "ngx-interactive-paycard-demo:build" 126 | } 127 | }, 128 | "test": { 129 | "builder": "@angular-devkit/build-angular:karma", 130 | "options": { 131 | "main": "projects/ngx-interactive-paycard-demo/src/test.ts", 132 | "polyfills": "projects/ngx-interactive-paycard-demo/src/polyfills.ts", 133 | "tsConfig": "projects/ngx-interactive-paycard-demo/tsconfig.spec.json", 134 | "karmaConfig": "projects/ngx-interactive-paycard-demo/karma.conf.js", 135 | "assets": [ 136 | "projects/ngx-interactive-paycard-demo/src/favicon.ico", 137 | "projects/ngx-interactive-paycard-demo/src/assets" 138 | ], 139 | "styles": [ 140 | "projects/ngx-interactive-paycard-demo/src/styles.scss" 141 | ], 142 | "scripts": [] 143 | } 144 | }, 145 | "lint": { 146 | "builder": "@angular-devkit/build-angular:tslint", 147 | "options": { 148 | "tsConfig": [ 149 | "projects/ngx-interactive-paycard-demo/tsconfig.app.json", 150 | "projects/ngx-interactive-paycard-demo/tsconfig.spec.json", 151 | "projects/ngx-interactive-paycard-demo/e2e/tsconfig.json" 152 | ], 153 | "exclude": [ 154 | "**/node_modules/**" 155 | ] 156 | } 157 | }, 158 | "e2e": { 159 | "builder": "@angular-devkit/build-angular:protractor", 160 | "options": { 161 | "protractorConfig": "projects/ngx-interactive-paycard-demo/e2e/protractor.conf.js", 162 | "devServerTarget": "ngx-interactive-paycard-demo:serve" 163 | }, 164 | "configurations": { 165 | "production": { 166 | "devServerTarget": "ngx-interactive-paycard-demo:serve:production" 167 | } 168 | } 169 | } 170 | } 171 | } 172 | }, 173 | "defaultProject": "ngx-interactive-paycard-lib" 174 | } 175 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/interactive-paycard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; 2 | 3 | import { CardLabel, FormLabel } from './shared'; 4 | import { CardModel } from './shared/card-model'; 5 | import { DefaultComponentLabels } from './shared/default-component-labels'; 6 | import { FocusedElement } from './shared/focused-element'; 7 | 8 | @Component({ 9 | selector: 'ngx-interactive-paycard', 10 | templateUrl: 'interactive-paycard.component.html', 11 | styleUrls: ['./interactive-paycard.component.scss'] 12 | }) 13 | export class InteractivePaycardComponent implements OnInit { 14 | 15 | @Input() chipImgPath: string; 16 | @Input() logoImgPath: string; 17 | @Input() backBgImgPath: string; 18 | @Input() frontBgImgPath: string; 19 | @Input() cardNumberFormat: string; 20 | @Input() cardNumberMask: string; 21 | @Input() 22 | get cardLabels(): CardLabel { return this._cardLabels; } 23 | set cardLabels(value: CardLabel | null) { 24 | this._cardLabels = value; 25 | } 26 | private _cardLabels: CardLabel = { 27 | expires: DefaultComponentLabels.CARD_EXPIRES, 28 | cardHolder: DefaultComponentLabels.CARD_HOLDER_NAME, 29 | fullName: DefaultComponentLabels.CARD_FULL_NAME, 30 | mm: DefaultComponentLabels.CARD_EXPIRATION_MONTH_FORMAT, 31 | yy: DefaultComponentLabels.CARD_EXPIRATION_YEAR_FORMAT 32 | }; 33 | 34 | @Input() 35 | get formLabels(): FormLabel { return this._formLabels; } 36 | set formLabels(value: FormLabel | null) { 37 | this._formLabels = value; 38 | } 39 | private _formLabels: FormLabel = { 40 | cardNumber: DefaultComponentLabels.FORM_CARD_NUMBER, 41 | cardHolderName: DefaultComponentLabels.FORM_CARD_HOLDER_NAME, 42 | expirationDate: DefaultComponentLabels.FORM_EXPIRATION_DATE, 43 | expirationMonth: DefaultComponentLabels.FORM_EXPIRATION_MONTH, 44 | expirationYear: DefaultComponentLabels.FORM_EXPIRATION_YEAR, 45 | cvv: DefaultComponentLabels.FORM_CVV, 46 | submitButton: DefaultComponentLabels.FORM_SUBMIT_BUTTON 47 | }; 48 | 49 | @Output() submitEvent = new EventEmitter(); 50 | @Output() changeCard = new EventEmitter(); 51 | @Output() changeCardNumber = new EventEmitter(); 52 | @ViewChild('cardNumberInput', { static: false }) cardNumberInputViewChild: ElementRef; 53 | 54 | cardModel: CardModel = { cardNumber: '', cardName: '', expirationMonth: '', expirationYear: '', cvv: '' }; 55 | 56 | cardNumberMaxLength = 19; 57 | minCardYear = new Date().getFullYear(); 58 | displayedCardNumber = this.cardModel.cardNumber; // The displayedCardNumber can be masked, the cardModel.cardNumber contains the real data 59 | displayedCvv = this.cardModel.cvv; // The displayed cvv can be masked 60 | 61 | cardNumberId = 'cardNumberId'; 62 | cardNameId = 'cardNameId'; 63 | monthSelect = 'monthSelect'; 64 | yearSelectId = 'yearSelectId'; 65 | cardCvvId = 'cardCvvId'; 66 | 67 | focusedElement: FocusedElement; 68 | cardNumberFormatArray: string[]; 69 | 70 | ngOnInit() { 71 | if (new RegExp('[^# ]').test(this.cardNumberFormat)) { 72 | throw new Error('The card number format must contain only "#" and " " characters! Check the "cardNumberFormat" input parameter!'); 73 | } 74 | if (new RegExp('[^# *]').test(this.cardNumberMask)) { 75 | throw new Error('The card number mask must contain only "#", "*" and " " characters! Check the "cardNumberMask" input parameter!'); 76 | } 77 | if (this.cardNumberMask.length !== this.cardNumberFormat.length) { 78 | throw new Error('The card number mask and the card number format must have the same length! \ 79 | Check the "cardNumberFormat" and the "cardNumberMask" input parameters!'); 80 | } 81 | this.cardNumberMaxLength = this.cardNumberFormat.length; 82 | this.cardNumberFormatArray = this.cardNumberFormat.split(''); 83 | } 84 | 85 | onCardNumberChange($event): void { 86 | let cursorPosStart = $event.srcElement.selectionStart; 87 | let cursorPosEnd = $event.srcElement.selectionEnd; 88 | let processedCardNumber: string = $event.target.value; 89 | const newValues: string[] = []; 90 | const letterRegex = new RegExp('[^0-9]'); 91 | const isCursorAtTheEnd = cursorPosEnd === processedCardNumber.length; 92 | const cardNumWithoutSpaceAsArray = processedCardNumber.replace(/ /g, '').split(''); 93 | this.cardNumberFormatArray.forEach((format) => { 94 | if (cardNumWithoutSpaceAsArray.length > 0) { 95 | if (format === '#') { 96 | let isNumber: boolean; 97 | let character: string; 98 | do { 99 | character = cardNumWithoutSpaceAsArray.shift(); 100 | isNumber = !(letterRegex.test(character)); 101 | if (isNumber) { 102 | newValues.push(character); 103 | } else { // don't move the cursor, if a letter is removed 104 | cursorPosEnd--; 105 | cursorPosStart--; 106 | } 107 | } while (!isNumber && character !== undefined); // find the next number 108 | } else if (format === ' ') { 109 | newValues.push(' '); 110 | } 111 | } 112 | }); 113 | processedCardNumber = newValues.join('').trim(); 114 | this.displayedCardNumber = processedCardNumber; 115 | this.cardModel.cardNumber = processedCardNumber; 116 | $event.target.value = processedCardNumber; // The value in event has to be updated, otherwise the letter remains in the 117 | if (!isCursorAtTheEnd) { // The cursor position has to be corrected because of the newly created string 118 | $event.srcElement.selectionEnd = cursorPosEnd; 119 | $event.srcElement.selectionStart = cursorPosStart; 120 | } 121 | this.onChangeCard(); 122 | this.onChangeCardNumber(); 123 | } 124 | 125 | onCvvChange(event): void { 126 | this.cardModel.cvv = event.target.value.replace(/[^0-9]*/g, ''); 127 | this.displayedCvv = this.cardModel.cvv; 128 | event.target.value = this.cardModel.cvv; 129 | this.onChangeCard(); 130 | } 131 | 132 | onCardNumberFocus(): void { 133 | this.unMaskCardNumber(); 134 | this.focusedElement = FocusedElement.CardNumber; 135 | } 136 | 137 | onCardNameFocus(): void { 138 | this.focusedElement = FocusedElement.CardName; 139 | } 140 | 141 | onDateFocus(): void { 142 | this.focusedElement = FocusedElement.ExpirationDate; 143 | } 144 | 145 | onCvvFocus(): void { 146 | this.unMaskCvv(); 147 | this.focusedElement = FocusedElement.CVV; 148 | } 149 | 150 | onBlur(): void { 151 | this.focusedElement = null; 152 | } 153 | 154 | onCardNumberBlur(): void { 155 | this.maskCardNumber(); 156 | this.onBlur(); 157 | } 158 | 159 | onCvvBlur(): void { 160 | this.maskCvv(); 161 | this.onBlur(); 162 | } 163 | 164 | onCardNameKeyPress($event): boolean { 165 | this.onChangeCard(); 166 | return (($event.charCode >= 65 && $event.charCode <= 90) || 167 | ($event.charCode >= 97 && $event.charCode <= 122) || ($event.charCode === 32)); 168 | } 169 | 170 | onMonthChange(): void { 171 | this.onChangeCard(); 172 | } 173 | 174 | onYearChange(): void { 175 | this.onChangeCard(); 176 | if (this.cardModel.expirationYear === this.minCardYear.toString()) { 177 | this.cardModel.expirationMonth = ''; 178 | } 179 | } 180 | 181 | onSubmitClick() { 182 | this.submitEvent.emit(this.cardModel); 183 | } 184 | 185 | onChangeCard() { 186 | this.changeCard.emit(this.cardModel); 187 | } 188 | 189 | onChangeCardNumber() { 190 | this.changeCardNumber.emit(this.cardModel.cardNumber); 191 | } 192 | 193 | minCardMonth(): number { 194 | if (this.cardModel.expirationYear === this.minCardYear.toString()) { 195 | return new Date().getMonth() + 1; 196 | } else { 197 | return 1; 198 | } 199 | } 200 | 201 | generateMonthValue(index: number): string { 202 | return index < 10 ? `0${index}` : index.toString(); 203 | } 204 | 205 | private maskCardNumber(): void { 206 | this.cardModel.cardNumber = this.displayedCardNumber; 207 | const arr = this.displayedCardNumber.split(''); 208 | arr.forEach((element, index) => { 209 | if (this.cardNumberMask[index] === '*') { 210 | arr[index] = '*'; 211 | } 212 | }); 213 | this.displayedCardNumber = arr.join(''); 214 | } 215 | 216 | private unMaskCardNumber(): void { 217 | this.displayedCardNumber = this.cardModel.cardNumber; 218 | } 219 | 220 | private unMaskCvv(): void { 221 | this.displayedCvv = this.cardModel.cvv; 222 | } 223 | 224 | private maskCvv(): void { 225 | this.displayedCvv = new Array(this.cardModel.cvv.length + 1).join('*'); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/card/card.component.scss: -------------------------------------------------------------------------------- 1 | .card-item { 2 | max-width: 430px; 3 | height: 270px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | position: relative; 7 | z-index: 2; 8 | width: 100%; 9 | 10 | @media screen and (max-width: 480px) { 11 | max-width: 310px; 12 | height: 220px; 13 | width: 90%; 14 | } 15 | 16 | @media screen and (max-width: 360px) { 17 | height: 180px; 18 | } 19 | 20 | &.-active { 21 | .card-item__side { 22 | &.-front { 23 | transform: perspective(1000px) rotateY(180deg) rotateX(0deg) rotateZ(0deg); 24 | } 25 | &.-back { 26 | transform: perspective(1000px) rotateY(0) rotateX(0deg) rotateZ(0deg); 27 | } 28 | } 29 | } 30 | 31 | &__focus { 32 | position: absolute; 33 | z-index: 3; 34 | border-radius: 5px; 35 | left: 0; 36 | top: 0; 37 | width: 100%; 38 | height: 100%; 39 | transition: all 0.35s cubic-bezier(0.71, 0.03, 0.56, 0.85); 40 | opacity: 0; 41 | pointer-events: none; 42 | overflow: hidden; 43 | border: 2px solid rgba(255, 255, 255, 0.65); 44 | 45 | &:after { 46 | content: ""; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | width: 100%; 51 | background: rgb(8, 20, 47); 52 | height: 100%; 53 | border-radius: 5px; 54 | filter: blur(25px); 55 | opacity: 0.5; 56 | } 57 | 58 | &.-active { 59 | opacity: 1; 60 | } 61 | } 62 | 63 | &__side { 64 | border-radius: 15px; 65 | overflow: hidden; 66 | box-shadow: 0 20px 60px 0 rgba(14, 42, 90, 0.55); 67 | transform: perspective(2000px) rotateY(0deg) rotateX(0deg) rotate(0deg); 68 | transform-style: preserve-3d; 69 | transition: all 0.8s cubic-bezier(0.71, 0.03, 0.56, 0.85); 70 | backface-visibility: hidden; 71 | height: 100%; 72 | 73 | &.-back { 74 | position: absolute; 75 | top: 0; 76 | left: 0; 77 | width: 100%; 78 | transform: perspective(2000px) rotateY(-180deg) rotateX(0deg) rotate(0deg); 79 | z-index: 2; 80 | padding: 0; 81 | height: 100%; 82 | 83 | .card-item__cover { 84 | transform: rotateY(-180deg); 85 | } 86 | } 87 | } 88 | &__bg { 89 | max-width: 100%; 90 | display: block; 91 | max-height: 100%; 92 | height: 100%; 93 | width: 100%; 94 | object-fit: cover; 95 | } 96 | &__cover { 97 | height: 100%; 98 | position: absolute; 99 | height: 100%; 100 | background-color: #1c1d27; 101 | background-image: linear-gradient(147deg, #1c1d27 0%, black 74%); 102 | left: 0; 103 | top: 0; 104 | width: 100%; 105 | border-radius: 15px; 106 | overflow: hidden; 107 | &:after { 108 | content: ""; 109 | position: absolute; 110 | left: 0; 111 | top: 0; 112 | width: 100%; 113 | height: 100%; 114 | background: rgba(6, 2, 29, 0.45); 115 | } 116 | } 117 | 118 | &__top { 119 | display: flex; 120 | align-items: flex-start; 121 | justify-content: space-between; 122 | margin-bottom: 40px; 123 | padding: 0 10px; 124 | 125 | @media screen and (max-width: 480px) { 126 | margin-bottom: 25px; 127 | } 128 | @media screen and (max-width: 360px) { 129 | margin-bottom: 15px; 130 | } 131 | } 132 | 133 | &__chip { 134 | width: 60px; 135 | @media screen and (max-width: 480px) { 136 | width: 50px; 137 | } 138 | @media screen and (max-width: 360px) { 139 | width: 40px; 140 | } 141 | } 142 | 143 | &__type { 144 | height: 45px; 145 | position: relative; 146 | display: flex; 147 | justify-content: flex-end; 148 | max-width: 100px; 149 | margin-left: auto; 150 | width: 100%; 151 | 152 | @media screen and (max-width: 480px) { 153 | height: 40px; 154 | max-width: 90px; 155 | } 156 | @media screen and (max-width: 360px) { 157 | height: 30px; 158 | } 159 | } 160 | 161 | &__typeImg { 162 | // max-width: 100%; 163 | object-fit: contain; 164 | // max-height: 100%; 165 | object-position: top right; 166 | } 167 | 168 | &__info { 169 | color: #fff; 170 | width: 100%; 171 | max-width: calc(100% - 85px); 172 | padding: 10px 15px; 173 | font-weight: 500; 174 | display: block; 175 | 176 | cursor: pointer; 177 | 178 | @media screen and (max-width: 480px) { 179 | padding: 10px; 180 | } 181 | } 182 | 183 | &__holder { 184 | opacity: 0.7; 185 | font-size: 13px; 186 | margin-bottom: 6px; 187 | @media screen and (max-width: 480px) { 188 | font-size: 12px; 189 | margin-bottom: 5px; 190 | } 191 | } 192 | 193 | &__wrapper { 194 | font-family: "Source Code Pro", monospace; 195 | padding: 25px 15px; 196 | position: relative; 197 | z-index: 4; 198 | height: 100%; 199 | text-shadow: 7px 6px 10px rgba(14, 42, 90, 0.8); 200 | user-select: none; 201 | @media screen and (max-width: 480px) { 202 | padding: 20px 10px; 203 | } 204 | } 205 | 206 | &__name { 207 | font-size: 18px; 208 | line-height: 1; 209 | white-space: nowrap; 210 | max-width: 100%; 211 | overflow: hidden; 212 | text-overflow: ellipsis; 213 | text-transform: uppercase; 214 | @media screen and (max-width: 480px) { 215 | font-size: 16px; 216 | } 217 | } 218 | &__nameItem { 219 | display: inline-block; 220 | min-width: 8px; 221 | position: relative; 222 | } 223 | 224 | &__number { 225 | font-weight: 500; 226 | line-height: 1; 227 | color: #fff; 228 | font-size: 27px; 229 | margin-bottom: 25px; 230 | display: inline-block; 231 | padding: 10px 15px; 232 | cursor: pointer; 233 | 234 | @media screen and (max-width: 480px) { 235 | font-size: 21px; 236 | margin-bottom: 15px; 237 | padding: 10px 10px; 238 | } 239 | 240 | @media screen and (max-width: 360px) { 241 | font-size: 19px; 242 | margin-bottom: 10px; 243 | padding: 10px 10px; 244 | } 245 | } 246 | 247 | &__numberItem { 248 | width: 16px; 249 | display: inline-block; 250 | &.-active { 251 | width: 30px; 252 | } 253 | 254 | @media screen and (max-width: 480px) { 255 | width: 13px; 256 | 257 | &.-active { 258 | width: 16px; 259 | } 260 | } 261 | 262 | @media screen and (max-width: 360px) { 263 | width: 12px; 264 | 265 | &.-active { 266 | width: 8px; 267 | } 268 | } 269 | } 270 | 271 | &__content { 272 | color: #fff; 273 | display: flex; 274 | align-items: flex-start; 275 | } 276 | 277 | &__date { 278 | flex-wrap: wrap; 279 | font-size: 18px; 280 | margin-left: auto; 281 | padding: 10px; 282 | display: inline-flex; 283 | width: 80px; 284 | white-space: nowrap; 285 | flex-shrink: 0; 286 | cursor: pointer; 287 | 288 | @media screen and (max-width: 480px) { 289 | font-size: 16px; 290 | } 291 | } 292 | 293 | &__dateItem { 294 | position: relative; 295 | span { 296 | width: 22px; 297 | display: inline-block; 298 | } 299 | } 300 | 301 | &__dateTitle { 302 | opacity: 0.7; 303 | font-size: 13px; 304 | padding-bottom: 6px; 305 | width: 100%; 306 | 307 | @media screen and (max-width: 480px) { 308 | font-size: 12px; 309 | padding-bottom: 5px; 310 | } 311 | } 312 | &__band { 313 | background: rgba(0, 0, 19, 0.8); 314 | width: 100%; 315 | height: 50px; 316 | margin-top: 30px; 317 | position: relative; 318 | z-index: 2; 319 | @media screen and (max-width: 480px) { 320 | margin-top: 20px; 321 | } 322 | @media screen and (max-width: 360px) { 323 | height: 40px; 324 | margin-top: 10px; 325 | } 326 | } 327 | 328 | &__cvv { 329 | text-align: right; 330 | position: relative; 331 | z-index: 2; 332 | padding: 15px; 333 | .card-item__type { 334 | opacity: 0.7; 335 | } 336 | 337 | @media screen and (max-width: 360px) { 338 | padding: 10px 15px; 339 | } 340 | } 341 | &__cvvTitle { 342 | padding-right: 10px; 343 | font-size: 15px; 344 | font-weight: 500; 345 | color: #fff; 346 | margin-bottom: 5px; 347 | } 348 | &__cvvBand { 349 | height: 45px; 350 | background: #fff; 351 | margin-bottom: 30px; 352 | text-align: right; 353 | display: flex; 354 | align-items: center; 355 | justify-content: flex-end; 356 | padding-right: 10px; 357 | color: #1a3b5d; 358 | font-size: 18px; 359 | border-radius: 4px; 360 | box-shadow: 0px 10px 20px -7px rgba(32, 56, 117, 0.35); 361 | 362 | @media screen and (max-width: 480px) { 363 | height: 40px; 364 | margin-bottom: 20px; 365 | } 366 | 367 | @media screen and (max-width: 360px) { 368 | margin-bottom: 15px; 369 | } 370 | } 371 | } 372 | 373 | .slide-fade-up-enter-active { 374 | transition: all 0.25s ease-in-out; 375 | transition-delay: 0.1s; 376 | position: relative; 377 | } 378 | .slide-fade-up-leave-active { 379 | transition: all 0.25s ease-in-out; 380 | position: absolute; 381 | } 382 | .slide-fade-up-enter { 383 | opacity: 0; 384 | transform: translateY(15px); 385 | pointer-events: none; 386 | } 387 | .slide-fade-up-leave-to { 388 | opacity: 0; 389 | transform: translateY(-15px); 390 | pointer-events: none; 391 | } 392 | 393 | .slide-fade-right-enter-active { 394 | transition: all 0.25s ease-in-out; 395 | transition-delay: 0.1s; 396 | position: relative; 397 | } 398 | .slide-fade-right-leave-active { 399 | transition: all 0.25s ease-in-out; 400 | position: absolute; 401 | } 402 | .slide-fade-right-enter { 403 | opacity: 0; 404 | transform: translateX(10px) rotate(45deg); 405 | pointer-events: none; 406 | } 407 | .slide-fade-right-leave-to { 408 | opacity: 0; 409 | transform: translateX(-10px) rotate(45deg); 410 | pointer-events: none; 411 | } 412 | 413 | .github-btn { 414 | position: absolute; 415 | right: 40px; 416 | bottom: 50px; 417 | text-decoration: none; 418 | padding: 15px 25px; 419 | border-radius: 4px; 420 | box-shadow: 0px 4px 30px -6px rgba(36, 52, 70, 0.65); 421 | background: #24292e; 422 | color: #fff; 423 | font-weight: bold; 424 | letter-spacing: 1px; 425 | font-size: 16px; 426 | text-align: center; 427 | transition: all 0.3s ease-in-out; 428 | 429 | @media screen and (min-width: 500px) { 430 | &:hover { 431 | transform: scale(1.1); 432 | box-shadow: 0px 17px 20px -6px rgba(36, 52, 70, 0.36); 433 | } 434 | } 435 | 436 | @media screen and (max-width: 700px) { 437 | position: relative; 438 | bottom: auto; 439 | right: auto; 440 | margin-top: 20px; 441 | 442 | &:active { 443 | transform: scale(1.1); 444 | box-shadow: 0px 17px 20px -6px rgba(36, 52, 70, 0.36); 445 | } 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /projects/ngx-interactive-paycard-lib/src/lib/interactive-paycard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { CardLabel, FormLabel } from './shared'; 3 | 4 | import { CommonModule } from '@angular/common'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | import { CardComponent } from './card/card.component'; 9 | import { InteractivePaycardComponent } from './interactive-paycard.component'; 10 | import { DefaultComponentLabels } from './shared/default-component-labels'; 11 | import { FocusedElement } from './shared/focused-element'; 12 | import { IfEveryChangesDirective } from './shared/if-every-changes.directive'; 13 | import { IfUndefinedChangesDirective } from './shared/if-undefined-changes.directive'; 14 | 15 | describe('InteractivePaycardComponent', () => { 16 | 17 | const formLabelsByDefaultMock: FormLabel = { 18 | cardNumber: DefaultComponentLabels.FORM_CARD_NUMBER, 19 | cardHolderName: DefaultComponentLabels.FORM_CARD_HOLDER_NAME, 20 | expirationDate: DefaultComponentLabels.FORM_EXPIRATION_DATE, 21 | expirationMonth: DefaultComponentLabels.FORM_EXPIRATION_MONTH, 22 | expirationYear: DefaultComponentLabels.FORM_EXPIRATION_YEAR, 23 | cvv: DefaultComponentLabels.FORM_CVV, 24 | submitButton: DefaultComponentLabels.FORM_SUBMIT_BUTTON 25 | }; 26 | const cardLabelsByDefaultMock: CardLabel = { 27 | expires: DefaultComponentLabels.CARD_EXPIRES, 28 | cardHolder: DefaultComponentLabels.CARD_HOLDER_NAME, 29 | fullName: DefaultComponentLabels.CARD_FULL_NAME, 30 | mm: DefaultComponentLabels.CARD_EXPIRATION_MONTH_FORMAT, 31 | yy: DefaultComponentLabels.CARD_EXPIRATION_YEAR_FORMAT 32 | }; 33 | beforeEach(async(() => { 34 | TestBed.configureTestingModule({ 35 | declarations: [InteractivePaycardComponent, CardComponent, IfUndefinedChangesDirective, IfEveryChangesDirective], 36 | imports: [ 37 | FormsModule, 38 | CommonModule, 39 | BrowserModule, 40 | BrowserAnimationsModule 41 | ] 42 | }) 43 | .compileComponents(); 44 | })); 45 | 46 | it('should create the component', () => { 47 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 48 | const component = fixture.componentInstance; 49 | component.cardNumberFormat = '#### #### #### ####'; 50 | component.cardNumberMask = '#### **** **** ####'; 51 | fixture.detectChanges(); 52 | expect(component).toBeTruthy(); 53 | }); 54 | 55 | it('should throw error if there is no card number format', () => { 56 | const errFixture = TestBed.createComponent(InteractivePaycardComponent); 57 | expect(() => { errFixture.detectChanges(); }).toThrow(); 58 | }); 59 | 60 | it('should throw error if there is no card number mask', () => { 61 | const errFixture = TestBed.createComponent(InteractivePaycardComponent); 62 | errFixture.componentInstance.cardNumberFormat = '#### #### #### ####'; 63 | expect(() => { errFixture.detectChanges(); }).toThrow(); 64 | }); 65 | 66 | it('should throw error if the mask format and card number format does not match', () => { 67 | const errFixture = TestBed.createComponent(InteractivePaycardComponent); 68 | errFixture.componentInstance.cardNumberFormat = '#### #### #### ####'; 69 | errFixture.componentInstance.cardNumberMask = '#### **** **** ###'; 70 | expect(() => { errFixture.detectChanges(); }).toThrow(); 71 | }); 72 | 73 | it('should show default values for form component labels', () => { 74 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 75 | const component = fixture.componentInstance; 76 | expect(component.formLabels).toEqual(formLabelsByDefaultMock); 77 | }); 78 | 79 | it('should show default values for card component labels', () => { 80 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 81 | const component = fixture.componentInstance; 82 | expect(component.cardLabels).toEqual(cardLabelsByDefaultMock); 83 | }); 84 | 85 | it('should emit an event when submit button is clicked', () => { 86 | // Arrange 87 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 88 | const component = fixture.componentInstance; 89 | spyOn(component.submitEvent, 'emit'); 90 | 91 | // Act 92 | component.onSubmitClick(); 93 | 94 | // Assert 95 | expect(component.submitEvent.emit).toHaveBeenCalled(); 96 | }); 97 | 98 | describe('minCardMonth', () => { 99 | let component: InteractivePaycardComponent; 100 | 101 | beforeEach(() => { 102 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 103 | component = fixture.componentInstance; 104 | }); 105 | 106 | it('should provide a valid month when card is expirong this year', () => { 107 | component.cardModel.expirationYear = '2000'; 108 | component.minCardYear = 2000; 109 | 110 | const thisMonth = new Date().getMonth(); 111 | expect(component.minCardMonth()).toEqual(thisMonth + 1); 112 | }); 113 | 114 | it('should return Jan as the default', () => { 115 | component.cardModel.expirationYear = '2000'; 116 | component.minCardYear = 2001; 117 | 118 | expect(component.minCardMonth()).toEqual(1); 119 | }); 120 | }); 121 | 122 | describe('generateMonthValue', () => { 123 | let component: InteractivePaycardComponent; 124 | 125 | beforeEach(() => { 126 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 127 | component = fixture.componentInstance; 128 | }); 129 | 130 | it('should prefix single digits with a zero', () => { 131 | [1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(i => { 132 | expect(component.generateMonthValue(i)).toEqual(`0${i}`); 133 | }); 134 | }); 135 | 136 | it('should convert double-digit numbers to strings', () => { 137 | [10, 11, 12].forEach(i => { 138 | expect(component.generateMonthValue(i)).toEqual(`${i}`); 139 | }); 140 | }); 141 | }); 142 | 143 | describe('onYearChange', () => { 144 | let component: InteractivePaycardComponent; 145 | 146 | beforeEach(() => { 147 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 148 | component = fixture.componentInstance; 149 | }); 150 | 151 | it('should reset the expiration month when the expiration year equals the minCardYear', () => { 152 | component.cardModel.expirationYear = '2000'; 153 | component.minCardYear = 2000; 154 | component.cardModel.expirationMonth = '04'; 155 | 156 | component.onYearChange(); 157 | expect(component.cardModel.expirationMonth).toEqual(''); 158 | }); 159 | 160 | it('should not reset the expiration month when the expiration year is not equal to the minCardYear', () => { 161 | component.cardModel.expirationYear = '2000'; 162 | component.minCardYear = 2001; 163 | component.cardModel.expirationMonth = '04'; 164 | 165 | component.onYearChange(); 166 | expect(component.cardModel.expirationMonth).toEqual('04'); 167 | }); 168 | }); 169 | 170 | describe('onCvvFocus', () => { 171 | let component: InteractivePaycardComponent; 172 | 173 | beforeEach(() => { 174 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 175 | component = fixture.componentInstance; 176 | }); 177 | 178 | it('should set the displayed CVV to what is stored in the model', () => { 179 | component.cardModel.cvv = '123'; 180 | expect(component.displayedCvv).toEqual(''); 181 | 182 | component.onCvvFocus(); 183 | expect(component.displayedCvv).toEqual(component.cardModel.cvv); 184 | }); 185 | 186 | it('should force focus to the CVV field', () => { 187 | expect(component.focusedElement).toBeUndefined(); 188 | 189 | component.onCvvFocus(); 190 | expect(component.focusedElement).toEqual(FocusedElement.CVV); 191 | }); 192 | }); 193 | 194 | describe('onCvvBlur', () => { 195 | let component: InteractivePaycardComponent; 196 | 197 | beforeEach(() => { 198 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 199 | component = fixture.componentInstance; 200 | }); 201 | 202 | it('should replace the displayed CVV value with a masked string of same length', () => { 203 | component.displayedCvv = '123'; 204 | component.cardModel.cvv = '123'; 205 | 206 | component.onCvvBlur(); 207 | expect(component.displayedCvv).toEqual('***'); 208 | 209 | component.displayedCvv = '12345'; 210 | component.cardModel.cvv = '12345'; 211 | 212 | component.onCvvBlur(); 213 | expect(component.displayedCvv).toEqual('*****'); 214 | }); 215 | 216 | it('should clear focus', () => { 217 | component.focusedElement = FocusedElement.CVV; 218 | 219 | component.onCvvBlur(); 220 | expect(component.focusedElement).toBeNull(); 221 | }); 222 | }); 223 | 224 | describe('onCardNumberFocus', () => { 225 | let component: InteractivePaycardComponent; 226 | 227 | beforeEach(() => { 228 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 229 | component = fixture.componentInstance; 230 | }); 231 | 232 | it('should set the displayed card number to what is stored in the model', () => { 233 | component.cardModel.cardNumber = '1234123412341234'; 234 | expect(component.displayedCardNumber).toEqual(''); 235 | 236 | component.onCardNumberFocus(); 237 | expect(component.displayedCardNumber).toEqual(component.cardModel.cardNumber); 238 | }); 239 | 240 | it('should force focus to the Card Number field', () => { 241 | expect(component.focusedElement).toBeUndefined(); 242 | 243 | component.onCardNumberFocus(); 244 | expect(component.focusedElement).toEqual(FocusedElement.CardNumber); 245 | }); 246 | }); 247 | 248 | describe('onCardNumberBlur', () => { 249 | let component: InteractivePaycardComponent; 250 | 251 | beforeEach(() => { 252 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 253 | component = fixture.componentInstance; 254 | }); 255 | 256 | it('should set the card number in the model equal to the display value', () => { 257 | const value = '1234123412341234'; 258 | component.displayedCardNumber = value; 259 | component.cardNumberMask = '************####'; 260 | component.cardModel.cardNumber = ''; 261 | 262 | component.onCardNumberBlur(); 263 | expect(component.cardModel.cardNumber).toEqual(value); 264 | }); 265 | 266 | it('should replace the displayed CardNumber value with a masked string following the card mask', () => { 267 | component.displayedCardNumber = '1234123412349876'; 268 | component.cardNumberMask = '************####'; 269 | 270 | component.onCardNumberBlur(); 271 | expect(component.displayedCardNumber).toEqual('************9876'); 272 | }); 273 | 274 | it('should clear focus', () => { 275 | component.focusedElement = FocusedElement.CardNumber; 276 | 277 | component.onCardNumberBlur(); 278 | expect(component.focusedElement).toBeNull(); 279 | }); 280 | }); 281 | 282 | describe('onCardNumberChange', () => { 283 | let component: InteractivePaycardComponent; 284 | 285 | beforeEach(() => { 286 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 287 | component = fixture.componentInstance; 288 | component.cardNumberFormatArray = ['#', '#', '#', '#', ' ', '#', '#', '#', '#', ' ', '#', '#', '#', '#', ' ', '#', '#', '#', '#']; 289 | }); 290 | 291 | it('should do nothing if the card number is empty', () => { 292 | const event = { 293 | srcElement: { 294 | selectionStart: 0, 295 | selectionEnd: 0, 296 | }, 297 | target: { 298 | value: '' 299 | } 300 | }; 301 | 302 | component.onCardNumberFocus(); 303 | component.onCardNumberChange(event); 304 | 305 | expect(component.displayedCardNumber).toEqual(event.target.value); 306 | expect(component.cardModel.cardNumber).toEqual(event.target.value); 307 | expect(event.srcElement.selectionStart).toBe(0); 308 | expect(event.srcElement.selectionEnd).toBe(0); 309 | }); 310 | 311 | it('should do nothing with inputs that match the mask', () => { 312 | const event = { 313 | srcElement: { 314 | selectionStart: 2, 315 | selectionEnd: 4, 316 | }, 317 | target: { 318 | value: '1234 2345 3456 7890' 319 | } 320 | }; 321 | 322 | component.onCardNumberFocus(); 323 | component.onCardNumberChange(event); 324 | 325 | expect(component.displayedCardNumber).toEqual(event.target.value); 326 | expect(component.cardModel.cardNumber).toEqual(event.target.value); 327 | expect(event.srcElement.selectionStart).toBe(2); 328 | expect(event.srcElement.selectionEnd).toBe(4); 329 | }); 330 | 331 | it('should remove a non-numerical character with inputs that match the mask', () => { 332 | const value = 'A1234 2345 3456 7890'; 333 | const event = { 334 | srcElement: { 335 | selectionStart: 2, 336 | selectionEnd: 4, 337 | }, 338 | target: { 339 | value: `${value}` // convert to new string so it can be overwritten 340 | } 341 | }; 342 | 343 | component.onCardNumberFocus(); 344 | component.onCardNumberChange(event); 345 | 346 | expect(component.displayedCardNumber).toEqual(value.slice(1)); 347 | expect(component.cardModel.cardNumber).toEqual(value.slice(1)); 348 | expect(event.target.value).toEqual(value.slice(1)); 349 | expect(event.srcElement.selectionStart).toBe(1); 350 | expect(event.srcElement.selectionEnd).toBe(3); 351 | }); 352 | }); 353 | 354 | describe('onCardNameFocus', () => { 355 | let component: InteractivePaycardComponent; 356 | 357 | beforeEach(() => { 358 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 359 | component = fixture.componentInstance; 360 | }); 361 | 362 | it('should force focus to the Card Name field', () => { 363 | expect(component.focusedElement).toBeUndefined(); 364 | component.onCardNameFocus(); 365 | expect(component.focusedElement).toEqual(FocusedElement.CardName); 366 | }); 367 | }); 368 | 369 | describe('onCardNameKeyPress', () => { 370 | let component: InteractivePaycardComponent; 371 | 372 | beforeEach(() => { 373 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 374 | component = fixture.componentInstance; 375 | }); 376 | 377 | it('should exclude ASCII characters 64 and below, but allow 32', () => { 378 | for (let i = 0; i < 65; i++) { 379 | expect(component.onCardNameKeyPress({ charCode: i })).toBe(i === 32); 380 | } 381 | }); 382 | it('should allow ASCII characters 65 thru 90', () => { 383 | for (let i = 65; i < 91; i++) { 384 | expect(component.onCardNameKeyPress({ charCode: i })).toBeTrue(); 385 | } 386 | }); 387 | it('should exclude ASCII characters 91 thru 96', () => { 388 | for (let i = 91; i < 96; i++) { 389 | expect(component.onCardNameKeyPress({ charCode: i })).toBeFalse(); 390 | } 391 | }); 392 | it('should allow ASCII characters 97 thru 122', () => { 393 | for (let i = 97; i < 123; i++) { 394 | expect(component.onCardNameKeyPress({ charCode: i })).toBeTrue(); 395 | } 396 | }); 397 | it('should exclude ASCII characters above 122', () => { 398 | for (let i = 123; i < 128; i++) { 399 | expect(component.onCardNameKeyPress({ charCode: i })).toBeFalse(); 400 | } 401 | }); 402 | }); 403 | 404 | describe('onDateFocus', () => { 405 | let component: InteractivePaycardComponent; 406 | 407 | beforeEach(() => { 408 | const fixture = TestBed.createComponent(InteractivePaycardComponent); 409 | component = fixture.componentInstance; 410 | }); 411 | 412 | it('should force focus to the Date field', () => { 413 | expect(component.focusedElement).toBeUndefined(); 414 | component.onDateFocus(); 415 | expect(component.focusedElement).toEqual(FocusedElement.ExpirationDate); 416 | }); 417 | }); 418 | }); 419 | --------------------------------------------------------------------------------