├── .eslintrc.json ├── .github └── workflows │ ├── deploy-demo.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .release-it.json ├── .yarn └── releases │ └── yarn-1.22.22.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── package.json ├── projects ├── angular-cc-library │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── ng-package.json │ ├── package.json │ ├── src │ │ ├── lib │ │ │ ├── credit-card-directives.module.ts │ │ │ ├── credit-card.spec.ts │ │ │ ├── credit-card.ts │ │ │ ├── directives │ │ │ │ ├── credit-card-format.directive.spec.ts │ │ │ │ ├── credit-card-format.directive.ts │ │ │ │ ├── cvc-format.directive.ts │ │ │ │ └── expiry-format.directive.ts │ │ │ └── validators.ts │ │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json └── example │ ├── .eslintrc.json │ ├── src │ ├── app │ │ ├── app.component.html │ │ └── app.component.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ └── styles.css │ └── tsconfig.app.json ├── stale.yml ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:@angular-eslint/recommended", 15 | "plugin:@angular-eslint/template/process-inline-templates" 16 | ], 17 | "rules": { 18 | "@angular-eslint/directive-selector": [ 19 | "error", 20 | { 21 | "type": "attribute", 22 | "prefix": "app", 23 | "style": "camelCase" 24 | } 25 | ], 26 | "@angular-eslint/component-selector": [ 27 | "error", 28 | { 29 | "type": "element", 30 | "prefix": "app", 31 | "style": "kebab-case" 32 | } 33 | ] 34 | } 35 | }, 36 | { 37 | "files": [ 38 | "*.html" 39 | ], 40 | "extends": [ 41 | "plugin:@angular-eslint/template/recommended", 42 | "plugin:@angular-eslint/template/accessibility" 43 | ], 44 | "rules": {} 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/deploy-demo.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Example site to Github Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: [master] 7 | workflow_dispatch: 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: 20 32 | cache: yarn 33 | - name: Setup Pages 34 | id: pages 35 | uses: actions/configure-pages@v4 36 | 37 | - name: Install dependencies 38 | run: yarn install 39 | 40 | - name: Build 41 | run: yarn build:demo --base-href {{steps.pages.base_path}} 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: './dist/example' 47 | 48 | # Deployment job 49 | deploy: 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | runs-on: ubuntu-latest 54 | needs: build 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-npm: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: yarn 21 | registry-url: https://registry.npmjs.org/ 22 | 23 | - name: Install dependencies and build 24 | run: | 25 | yarn install 26 | yarn build:library 27 | 28 | - name: git config 29 | run: | 30 | git config user.name "${GITHUB_ACTOR}" 31 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 32 | 33 | - name: npm config 34 | run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 35 | 36 | - name: Bump version 37 | run: npx release-it --increment 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.angular/cache 2 | .idea/ 3 | typings/ 4 | node_modules/ 5 | dist/ 6 | .DS_Store 7 | npm-debug.log 8 | compiled/ 9 | *.metadata.json 10 | *.ngFactory.ts 11 | *.js 12 | *.js.map 13 | !/index.js 14 | src/**/*.js 15 | src/**/*.map 16 | src/**/*.json 17 | *.tgz 18 | aot/* 19 | lib/* 20 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn precommit 2 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "push": true, 4 | "commit": true, 5 | "commitMessage": "chore: version ${version} [skip ci]", 6 | "requireBranch": "master", 7 | "tag": true 8 | }, 9 | "github": { 10 | "release": true, 11 | "autoGenerate": true, 12 | "releaseName": "${version}" 13 | }, 14 | "npm": { 15 | "publish": true, 16 | "publishPath": "./dist/angular-cc-library" 17 | }, 18 | "plugins": { 19 | "@release-it/conventional-changelog": { 20 | "preset": { 21 | "name": "conventionalcommits", 22 | "types": [ 23 | { 24 | "type": "feat", 25 | "section": "Features" 26 | }, 27 | { 28 | "type": "fix", 29 | "section": "Bug Fixes" 30 | }, 31 | {} 32 | ] 33 | }, 34 | "infile": "CHANGELOG.md", 35 | "header": "# Change Log" 36 | }, 37 | "@release-it/bumper": { 38 | "out": ["dist/angular-cc-library/package.json", "projects/angular-cc-library/package.json"] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-1.22.22.cjs -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [3.4.0](https://github.com/timofei-iatsenko/angular-cc-library/compare/v3.3.0...v3.4.0) (2024-09-10) 4 | 5 | 6 | ### Features 7 | 8 | * update to angular 18 ([39fb63b](https://github.com/timofei-iatsenko/angular-cc-library/commit/39fb63b45eeced1cfa3d3a44d6f3531fc4c3a27a)) 9 | 10 | ## [3.3.0](https://github.com/thekip/angular-cc-library/compare/v3.2.0...v3.3.0) (2024-02-02) 11 | 12 | 13 | ### Features 14 | 15 | * support standalone directives ([afbcbc7](https://github.com/thekip/angular-cc-library/commit/afbcbc77001c4113876ca23c7ccb1eaec4d8f29d)) 16 | * update to angular v17 ([c5d510b](https://github.com/thekip/angular-cc-library/commit/c5d510bcae23c6245bff0db5d9cdce08b0276e96)) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 YangYang Yu 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Angular CC Library

3 | 4 | Validation and formatting input parameters of Credit Cards 5 | 6 |
7 | 8 | [![Version][badge-version]][package] 9 | [![Downloads][badge-downloads]][package] 10 | 11 |
12 | 13 | # Demo 14 | 15 | https://timofei-iatsenko.github.io/angular-cc-library/ 16 | 17 | Or run locally: 18 | 19 | 1. Clone repo 20 | 2. run `yarn install` 21 | 3. run `yarn run:demo` 22 | 4. visit `http://localhost:4200` 23 | 24 | # Usage 25 | 26 | ## Installation 27 | ```shell 28 | npm install angular-cc-library --save 29 | ``` 30 | 31 | ## Version Compatibility 32 | 33 | | Angular | Library | 34 | |---------|---------| 35 | | 18.x | 3.4.x | 36 | | 17.x | 3.3.x | 37 | | 16.x | 3.2.x | 38 | | 15.x | 3.1.x | 39 | | 14.x | 3.0.4 | 40 | | 13.x | 3.0.0 | 41 | | 12.x | 2.1.3 | 42 | 43 | 44 | ## Formatting Directive 45 | On the input fields, add the specific directive to format inputs. 46 | All fields must be `type='tel'` in order to support spacing and additional characters. 47 | 48 | Since `angular-cc-library@3.3.0` all directives declared as standalone, 49 | so you can import them directly into your component: 50 | 51 | ```typescript 52 | import { Component } from '@angular/core'; 53 | import { CreditCardFormatDirective } from 'angular-cc-library'; 54 | 55 | @Component({ 56 | selector: 'credit-card-number-input', 57 | standalone: true, 58 | deps: [CreditCardFormatDirective], 59 | template: `` 60 | }) 61 | export class CreditCardNumberInputComponent {} 62 | ``` 63 | 64 | But you can still import them all at once using `CreditCardDirectivesModule`: 65 | 66 | ```typescript 67 | import { NgModule } from '@angular/core'; 68 | import { BrowserModule } from '@angular/platform-browser'; 69 | import { FormsModule } from '@angular/forms'; 70 | import { CreditCardDirectivesModule } from 'angular-cc-library'; 71 | 72 | import { AppComponent } from './app.component'; 73 | 74 | @NgModule({ 75 | imports: [BrowserModule, FormsModule, CreditCardDirectivesModule], 76 | declarations: [AppComponent], 77 | bootstrap: [AppComponent] 78 | }) 79 | export class AppModule { 80 | } 81 | ``` 82 | 83 | **Credit Card Formatter** 84 | * add `ccNumber` directive: 85 | ```html 86 | 87 | ``` 88 | * this will also apply a class name based off the card `.visa`, `.amex`, etc. See the array of card types in `credit-card.ts` for all available types 89 | 90 | * You can get parsed card type by using export api: 91 | 92 | ```html 93 | 94 | {{ccNumber.resolvedScheme$ | async}} 95 | ``` 96 | 97 | `resolvedScheme$` will be populated with `visa`, `amex`, etc. 98 | 99 | 100 | **Expiration Date Formatter** 101 | Will support format of MM/YY or MM/YYYY 102 | * add `ccExp` directive: 103 | ```html 104 | 105 | ``` 106 | 107 | **CVC Formater** 108 | * add `ccCVC` directive: 109 | ```html 110 | 111 | ``` 112 | 113 | ### Validation 114 | Current only Model Validation is supported. 115 | To implement, import the validator library and apply the specific validator on each form control 116 | 117 | ```typescript 118 | import { CreditCardValidators } from 'angular-cc-library'; 119 | 120 | @Component({ 121 | selector: 'app', 122 | template: ` 123 |
124 | 125 | 126 | 127 |
128 | ` 129 | }) 130 | export class AppComponent implements OnInit { 131 | form: FormGroup; 132 | submitted: boolean = false; 133 | 134 | constructor(private _fb: FormBuilder) {} 135 | 136 | ngOnInit() { 137 | this.form = this._fb.group({ 138 | creditCard: ['', [CreditCardValidators.validateCCNumber]], 139 | expirationDate: ['', [CreditCardValidators.validateExpDate]], 140 | cvc: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(4)]] 141 | }); 142 | } 143 | 144 | onSubmit(form) { 145 | this.submitted = true; 146 | console.log(form); 147 | } 148 | } 149 | ``` 150 | 151 | # Inspiration 152 | 153 | Based on Stripe's [jquery.payment](https://github.com/stripe/jquery.payment) plugin but adapted for use by Angular 154 | 155 | # License 156 | 157 | MIT 158 | 159 | [badge-downloads]: https://img.shields.io/npm/dw/angular-cc-library.svg 160 | [badge-version]: https://img.shields.io/npm/v/angular-cc-library.svg 161 | [package]: https://www.npmjs.com/package/angular-cc-library 162 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "yarn", 6 | "analytics": "d3a9c97d-1dcd-4643-99bd-20d73f0a2164", 7 | "schematicCollections": [ 8 | "@angular-eslint/schematics" 9 | ] 10 | }, 11 | "newProjectRoot": "projects", 12 | "projects": { 13 | "angular-cc-library": { 14 | "projectType": "library", 15 | "root": "projects/angular-cc-library", 16 | "sourceRoot": "projects/angular-cc-library/src", 17 | "prefix": "lib", 18 | "architect": { 19 | "build": { 20 | "builder": "@angular-devkit/build-angular:ng-packagr", 21 | "options": { 22 | "tsConfig": "projects/angular-cc-library/tsconfig.lib.json", 23 | "project": "projects/angular-cc-library/ng-package.json" 24 | }, 25 | "configurations": { 26 | "production": { 27 | "tsConfig": "projects/angular-cc-library/tsconfig.lib.prod.json" 28 | } 29 | } 30 | }, 31 | "test": { 32 | "builder": "@angular-builders/jest:run", 33 | "options": { 34 | "tsConfig": "tsconfig.spec.json" 35 | } 36 | }, 37 | "lint": { 38 | "builder": "@angular-eslint/builder:lint", 39 | "options": { 40 | "lintFilePatterns": [ 41 | "projects/angular-cc-library/**/*.ts", 42 | "projects/angular-cc-library/**/*.html" 43 | ] 44 | } 45 | }, 46 | "deploy": { 47 | "builder": "ngx-deploy-npm:deploy", 48 | "options": { 49 | "access": "public" 50 | } 51 | } 52 | } 53 | }, 54 | "example": { 55 | "projectType": "application", 56 | "schematics": {}, 57 | "root": "projects/example", 58 | "sourceRoot": "projects/example/src", 59 | "prefix": "app", 60 | "architect": { 61 | "build": { 62 | "builder": "@angular-devkit/build-angular:browser", 63 | "options": { 64 | "outputPath": "dist/example", 65 | "index": "projects/example/src/index.html", 66 | "main": "projects/example/src/main.ts", 67 | "polyfills": "projects/example/src/polyfills.ts", 68 | "tsConfig": "projects/example/tsconfig.app.json", 69 | "assets": [ 70 | "projects/example/src/favicon.ico", 71 | "projects/example/src/assets" 72 | ], 73 | "styles": [ 74 | "projects/example/src/styles.css" 75 | ], 76 | "scripts": [], 77 | "vendorChunk": true, 78 | "extractLicenses": false, 79 | "buildOptimizer": false, 80 | "sourceMap": true, 81 | "optimization": false, 82 | "namedChunks": true 83 | }, 84 | "configurations": { 85 | "production": { 86 | "fileReplacements": [ 87 | { 88 | "replace": "projects/example/src/environments/environment.ts", 89 | "with": "projects/example/src/environments/environment.prod.ts" 90 | } 91 | ], 92 | "optimization": true, 93 | "outputHashing": "all", 94 | "sourceMap": false, 95 | "namedChunks": false, 96 | "extractLicenses": true, 97 | "vendorChunk": false, 98 | "buildOptimizer": true, 99 | "budgets": [ 100 | { 101 | "type": "initial", 102 | "maximumWarning": "2mb", 103 | "maximumError": "5mb" 104 | }, 105 | { 106 | "type": "anyComponentStyle", 107 | "maximumWarning": "6kb", 108 | "maximumError": "10kb" 109 | } 110 | ] 111 | } 112 | }, 113 | "defaultConfiguration": "" 114 | }, 115 | "serve": { 116 | "builder": "@angular-devkit/build-angular:dev-server", 117 | "options": { 118 | "buildTarget": "example:build" 119 | }, 120 | "configurations": { 121 | "production": { 122 | "buildTarget": "example:build:production" 123 | } 124 | } 125 | }, 126 | "extract-i18n": { 127 | "builder": "@angular-devkit/build-angular:extract-i18n", 128 | "options": { 129 | "buildTarget": "example:build" 130 | } 131 | }, 132 | "e2e": { 133 | "builder": "@angular-devkit/build-angular:protractor", 134 | "options": { 135 | "protractorConfig": "projects/example/e2e/protractor.conf.js", 136 | "devServerTarget": "example:serve" 137 | }, 138 | "configurations": { 139 | "production": { 140 | "devServerTarget": "example:serve:production" 141 | } 142 | } 143 | }, 144 | "lint": { 145 | "builder": "@angular-eslint/builder:lint", 146 | "options": { 147 | "lintFilePatterns": [ 148 | "projects/example/**/*.ts", 149 | "projects/example/**/*.html" 150 | ] 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cc-library", 3 | "version": "3.4.0", 4 | "scripts": { 5 | "run:demo": "ng serve example", 6 | "build:demo": "ng build example --configuration production", 7 | "build:library": "ng build angular-cc-library --configuration production ", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "deploy": "yarn build:library && cd ./dist/angular-cc-library && npm publish", 12 | "precommit": "lint-staged", 13 | "prepare": "husky" 14 | }, 15 | "dependencies": { 16 | "@angular/common": "^18.2.2", 17 | "@angular/compiler": "^18.2.2", 18 | "@angular/core": "^18.2.2", 19 | "@angular/forms": "^18.2.2", 20 | "@angular/platform-browser": "^18.2.2", 21 | "@angular/platform-browser-dynamic": "^18.2.2", 22 | "rxjs": "~7.8.1", 23 | "tslib": "^2.7.0", 24 | "zone.js": "~0.14.10" 25 | }, 26 | "devDependencies": { 27 | "@angular-builders/jest": "18.0.0", 28 | "@angular-devkit/build-angular": "^18.2.2", 29 | "@angular-eslint/builder": "18.3.0", 30 | "@angular-eslint/eslint-plugin": "18.3.0", 31 | "@angular-eslint/eslint-plugin-template": "18.3.0", 32 | "@angular-eslint/schematics": "18.3.0", 33 | "@angular-eslint/template-parser": "18.3.0", 34 | "@angular/cli": "^18.2.2", 35 | "@angular/compiler-cli": "^18.2.2", 36 | "@angular/language-service": "^18.2.2", 37 | "@release-it/bumper": "^6.0.1", 38 | "@release-it/conventional-changelog": "^8.0.1", 39 | "@types/jest": "^29.5.12", 40 | "@types/node": "^22.5.3", 41 | "@typescript-eslint/eslint-plugin": "8.4.0", 42 | "@typescript-eslint/parser": "8.4.0", 43 | "eslint": "^8.56.0", 44 | "husky": "^9.1.5", 45 | "jest": "29.7.0", 46 | "lint-staged": "^15.2.10", 47 | "ng-packagr": "^18.2.1", 48 | "release-it": "^17.6.0", 49 | "ts-node": "~10.9.2", 50 | "typescript": "~5.5.4" 51 | }, 52 | "lint-staged": { 53 | "*.{ts,component.html}": [ 54 | "eslint --fix" 55 | ] 56 | }, 57 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 58 | } 59 | -------------------------------------------------------------------------------- /projects/angular-cc-library/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "cc", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "cc", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": [ 32 | "*.html" 33 | ], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /projects/angular-cc-library/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /projects/angular-cc-library/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | 'ts-jest': { 4 | allowSyntheticDefaultImports: false, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /projects/angular-cc-library/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-cc-library", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/angular-cc-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-cc-library", 3 | "version": "3.4.0", 4 | "description": "angular credit card library", 5 | "keywords": [ 6 | "angular", 7 | "validate", 8 | "validation", 9 | "credit card", 10 | "credit card format" 11 | ], 12 | "author": "nogorilla ", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/timofei-iatsenko/angular-cc-library/issues" 16 | }, 17 | "homepage": "https://github.com/timofei-iatsenko/angular-cc-library#readme", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/timofei-iatsenko/angular-cc-library.git" 21 | }, 22 | "dependencies": { 23 | "tslib": "^2.4.1" 24 | }, 25 | "peerDependencies": { 26 | "@angular/common": "^18.0.0", 27 | "@angular/core": "^18.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/credit-card-directives.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { CreditCardFormatDirective } from './directives/credit-card-format.directive'; 4 | import { ExpiryFormatDirective } from './directives/expiry-format.directive'; 5 | import { CvcFormatDirective } from './directives/cvc-format.directive'; 6 | 7 | const CREDIT_CARD_LIBRARY_DIRECTIVES = [ 8 | CreditCardFormatDirective, 9 | ExpiryFormatDirective, 10 | CvcFormatDirective, 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [CREDIT_CARD_LIBRARY_DIRECTIVES], 15 | exports: [CREDIT_CARD_LIBRARY_DIRECTIVES], 16 | }) 17 | export class CreditCardDirectivesModule { 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/credit-card.spec.ts: -------------------------------------------------------------------------------- 1 | import { CreditCard } from './credit-card'; 2 | 3 | describe('Shared: Credit Card', () => { 4 | it('should return a card object by number', () => { 5 | const num = '4111111111111111'; 6 | 7 | expect(CreditCard.cardFromNumber(num)).toEqual({ 8 | type: 'visa', 9 | patterns: [4], 10 | format: /(\d{1,4})/g, 11 | length: [13, 16, 19], 12 | cvvLength: [3], 13 | luhn: true, 14 | }); 15 | }); 16 | 17 | it('should restrict numeric', () => { 18 | 19 | const valid: Partial = { 20 | which: 49, // key press 1 21 | }; 22 | const metaKey: Partial = { 23 | metaKey: true, 24 | }; 25 | const invalid: Partial = { 26 | which: 32, 27 | }; 28 | expect(CreditCard.restrictNumeric(valid as KeyboardEvent)).toBe(true); 29 | expect(CreditCard.restrictNumeric(metaKey as KeyboardEvent)).toBe(true); 30 | expect(CreditCard.restrictNumeric(invalid as KeyboardEvent)).toBe(false); 31 | }); 32 | 33 | it('should determine text selected', () => { 34 | const target: Partial = { 35 | selectionStart: 1, 36 | selectionEnd: 2, 37 | }; 38 | expect(CreditCard.hasTextSelected(target as HTMLInputElement)).toBe(true); 39 | 40 | target.selectionStart = null; 41 | expect(CreditCard.hasTextSelected(target as HTMLInputElement)).toBe(false); 42 | 43 | target.selectionStart = 1; 44 | target.selectionEnd = 1; 45 | expect(CreditCard.hasTextSelected(target as HTMLInputElement)).toBe(false); 46 | }); 47 | 48 | it('should return card type', () => { 49 | expect(CreditCard.cardType(null)).toBeFalsy(); 50 | 51 | const num = '4111111111111111'; 52 | expect(CreditCard.cardType(num)).toBe('visa'); 53 | }); 54 | 55 | it('should format a card number', () => { 56 | const num = '4111111111111111'; 57 | expect(CreditCard.formatCardNumber(num)).toBe('4111 1111 1111 1111'); 58 | }); 59 | 60 | xit('should return a safe value', () => { 61 | const value = ''; 62 | let target: Partial = { 63 | selectionStart: 1, 64 | selectionEnd: 2, 65 | }; 66 | 67 | expect(CreditCard.safeVal(value, target as HTMLInputElement, (val) => target.value = val)).toBe(null); 68 | 69 | const element = document.createElement('input'); 70 | document.body.appendChild(element); 71 | element.focus(); 72 | 73 | target = element; 74 | target.selectionStart = 1; 75 | target.value = '4111111111111111'; 76 | 77 | expect(CreditCard.safeVal(target.value, target as HTMLInputElement, (val) => target.value = val)).toBe(16); 78 | 79 | }); 80 | 81 | it('should restrict card number', () => { 82 | const key = 49; 83 | const target: Partial = { 84 | selectionStart: null, 85 | value: '411111111111111', 86 | }; 87 | 88 | expect(CreditCard.isCardNumber(key, target as HTMLInputElement)).toBe(true); 89 | 90 | target.value = '41111111111111111111'; 91 | expect(CreditCard.isCardNumber(key, target as HTMLInputElement)).toBe(false); 92 | 93 | }); 94 | 95 | it('should restrict expiry', () => { 96 | let key = 1; 97 | const target: Partial = { 98 | selectionStart: null, 99 | value: '12 / 12', 100 | }; 101 | 102 | expect(CreditCard.restrictExpiry(key, target as HTMLInputElement)).toBe(false); 103 | 104 | key = 49; 105 | expect(CreditCard.restrictExpiry(key, target as HTMLInputElement)).toBe(false); 106 | 107 | target.value = '12 / 1234'; 108 | expect(CreditCard.restrictExpiry(key, target as HTMLInputElement)).toBe(true); 109 | 110 | }); 111 | 112 | it('should replace full width characters', () => { 113 | expect(CreditCard.replaceFullWidthChars(null)).toBe(''); 114 | 115 | const str = '0123456789'; 116 | expect(CreditCard.replaceFullWidthChars(str)).toBe('0123456789'); 117 | }); 118 | 119 | it('should format expiration date', () => { 120 | expect(CreditCard.formatExpiry('1234')).toBe('12 / 34'); 121 | 122 | expect(CreditCard.formatExpiry('123456')).toBe('12 / 3456'); 123 | }); 124 | 125 | it('should restrict CVV', () => { 126 | let key = 1; 127 | const target: Partial = { 128 | selectionStart: null, 129 | value: '123', 130 | }; 131 | 132 | expect(CreditCard.restrictCvc(key, target as HTMLInputElement)).toBe(false); 133 | 134 | key = 49; 135 | expect(CreditCard.restrictCvc(key, target as HTMLInputElement)).toBe(true); 136 | 137 | target.value = '1234'; 138 | expect(CreditCard.restrictCvc(key, target as HTMLInputElement)).toBe(false); 139 | 140 | }); 141 | 142 | it('should check for luhn value', () => { 143 | expect(CreditCard.luhnCheck('4111111111111111')).toBe(true); 144 | 145 | expect(CreditCard.luhnCheck('4511111111111111')).toBe(false); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/credit-card.ts: -------------------------------------------------------------------------------- 1 | const defaultFormat = /(\d{1,4})/g; 2 | 3 | export interface CardDefinition { 4 | type: string; 5 | patterns: number[]; 6 | format: RegExp; 7 | length: number[]; 8 | cvvLength: number[]; 9 | luhn: boolean; 10 | } 11 | 12 | const cards: CardDefinition[] = [ 13 | { 14 | type: 'maestro', 15 | patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67], 16 | format: defaultFormat, 17 | length: [12, 13, 14, 15, 16, 17, 18, 19], 18 | cvvLength: [3], 19 | luhn: true, 20 | }, { 21 | type: 'forbrugsforeningen', 22 | patterns: [600], 23 | format: defaultFormat, 24 | length: [16], 25 | cvvLength: [3], 26 | luhn: true, 27 | }, { 28 | type: 'dankort', 29 | patterns: [5019], 30 | format: defaultFormat, 31 | length: [16], 32 | cvvLength: [3], 33 | luhn: true, 34 | }, { 35 | type: 'visa', 36 | patterns: [4], 37 | format: defaultFormat, 38 | length: [13, 16, 19], 39 | cvvLength: [3], 40 | luhn: true, 41 | }, { 42 | type: 'mastercard', 43 | patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27], 44 | format: defaultFormat, 45 | length: [16], 46 | cvvLength: [3], 47 | luhn: true, 48 | }, { 49 | type: 'amex', 50 | patterns: [34, 37], 51 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 52 | length: [15], 53 | cvvLength: [3, 4], 54 | luhn: true, 55 | }, { 56 | type: 'dinersclub', 57 | patterns: [30, 36, 38, 39], 58 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 59 | length: [14], 60 | cvvLength: [3], 61 | luhn: true, 62 | }, { 63 | type: 'discover', 64 | patterns: [60, 64, 65, 622], 65 | format: defaultFormat, 66 | length: [16], 67 | cvvLength: [3], 68 | luhn: true, 69 | }, { 70 | type: 'unionpay', 71 | patterns: [62, 88], 72 | format: defaultFormat, 73 | length: [16, 17, 18, 19], 74 | cvvLength: [3], 75 | luhn: false, 76 | }, { 77 | type: 'jcb', 78 | patterns: [35], 79 | format: defaultFormat, 80 | length: [16, 19], 81 | cvvLength: [3], 82 | luhn: true, 83 | }, 84 | ]; 85 | 86 | // @dynamic 87 | export class CreditCard { 88 | 89 | public static cards() { 90 | return cards; 91 | } 92 | 93 | public static cardFromNumber(num: string): CardDefinition { 94 | num = (num + '').replace(/\D/g, ''); 95 | 96 | for (let i = 0, len = cards.length; i < len; i++) { 97 | const card = cards[i]; 98 | const ref = card.patterns; 99 | 100 | for (let j = 0, len1 = ref.length; j < len1; j++) { 101 | const pattern = ref[j]; 102 | const p = pattern + ''; 103 | 104 | if (num.substr(0, p.length) === p) { 105 | return card; 106 | } 107 | } 108 | } 109 | } 110 | 111 | public static restrictNumeric(e: KeyboardEvent): boolean { 112 | if (e.metaKey || e.ctrlKey) { 113 | return true; 114 | } 115 | if (e.which === 32) { 116 | return false; 117 | } 118 | if (e.which === 0) { 119 | return true; 120 | } 121 | if (e.which < 33) { 122 | return true; 123 | } 124 | const input = String.fromCharCode(e.which); 125 | return !!/[\d\s]/.test(input); 126 | } 127 | 128 | public static hasTextSelected(target: HTMLInputElement) { 129 | return target.selectionStart !== null && target.selectionStart !== target.selectionEnd; 130 | } 131 | 132 | public static cardType(num: string) { 133 | if (!num) { 134 | return num; 135 | } 136 | 137 | const card = this.cardFromNumber(num); 138 | 139 | if (card !== null && typeof card !== 'undefined') { 140 | return card.type; 141 | } else { 142 | return null; 143 | } 144 | } 145 | 146 | public static formatCardNumber(num: string) { 147 | num = num.replace(/\D/g, ''); 148 | const card = this.cardFromNumber(num); 149 | 150 | if (!card) { 151 | return num; 152 | } 153 | 154 | const upperLength = card.length[card.length.length - 1]; 155 | num = num.slice(0, upperLength); 156 | 157 | if (card.format.global) { 158 | const matches = num.match(card.format); 159 | if (matches != null) { 160 | return matches.join(' '); 161 | } 162 | } else { 163 | const groups = card.format.exec(num); 164 | if (groups == null) { 165 | return; 166 | } 167 | groups.shift(); 168 | return groups.filter(Boolean).join(' '); 169 | } 170 | } 171 | 172 | public static safeVal(value: string, target: HTMLInputElement, updateValue: (value: string) => void): number { 173 | let cursor: number | null = null; 174 | const last = target.value; 175 | let result: number = null; 176 | 177 | try { 178 | cursor = target.selectionStart; 179 | } catch (error) { 180 | // do nothing 181 | } 182 | 183 | updateValue(value); 184 | 185 | if (cursor !== null && target === document.activeElement) { 186 | if (cursor === last.length) { 187 | cursor = value.length; 188 | } 189 | 190 | if (last !== value) { 191 | const prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9); 192 | const currPair = value.slice(cursor - 1, +cursor + 1 || 9e9); 193 | const digit = value[cursor]; 194 | 195 | if (/\d/.test(digit) && prevPair === (`${digit} `) && currPair === (` ${digit}`)) { 196 | cursor = cursor + 1; 197 | } 198 | } 199 | 200 | result = cursor; 201 | } 202 | return result; 203 | } 204 | 205 | public static isCardNumber(key: number, target: HTMLInputElement): boolean { 206 | const digit = String.fromCharCode(key); 207 | if (!/^\d+$/.test(digit)) { 208 | return false; 209 | } 210 | if (CreditCard.hasTextSelected(target)) { 211 | return true; 212 | } 213 | const value = (target.value + digit).replace(/\D/g, ''); 214 | const card = CreditCard.cardFromNumber(value); 215 | 216 | if (card) { 217 | return value.length <= card.length[card.length.length - 1]; 218 | } else { 219 | return value.length <= 16; 220 | } 221 | } 222 | 223 | public static restrictExpiry(key: number, target: HTMLInputElement) { 224 | const digit = String.fromCharCode(key); 225 | if (!/^\d+$/.test(digit) || this.hasTextSelected(target)) { 226 | return false; 227 | } 228 | const value = `${target.value}${digit}`.replace(/\D/g, ''); 229 | 230 | return value.length > 6; 231 | } 232 | 233 | public static replaceFullWidthChars(str: string) { 234 | if (str === null) { 235 | str = ''; 236 | } 237 | 238 | const fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 239 | const halfWidth = '0123456789'; 240 | let value = ''; 241 | const chars = str.split(''); 242 | 243 | for (let i = 0; i < chars.length; i++) { 244 | let chr = chars[i]; 245 | const idx = fullWidth.indexOf(chr); 246 | if (idx > -1) { 247 | chr = halfWidth[idx]; 248 | } 249 | value += chr; 250 | } 251 | return value; 252 | } 253 | 254 | public static formatExpiry(expiry: string) { 255 | const parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 256 | 257 | if (!parts) { 258 | return ''; 259 | } 260 | 261 | let mon = parts[1] || ''; 262 | let sep = parts[2] || ''; 263 | const year = parts[3] || ''; 264 | 265 | if (year.length > 0) { 266 | sep = ' / '; 267 | } else if (sep === ' /') { 268 | mon = mon.substring(0, 1); 269 | sep = ''; 270 | } else if (mon.length === 2 || sep.length > 0) { 271 | sep = ' / '; 272 | } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { 273 | mon = `0${mon}`; 274 | sep = ' / '; 275 | } 276 | return `${mon}${sep}${year}`; 277 | } 278 | 279 | public static restrictCvc(key: number, target: HTMLInputElement) { 280 | const digit = String.fromCharCode(key); 281 | if (!/^\d+$/.test(digit) || this.hasTextSelected(target)) { 282 | return false; 283 | } 284 | const val = `${target.value}${digit}`; 285 | return val.length <= 4; 286 | } 287 | 288 | public static luhnCheck(num: string) { 289 | const digits = num.split('').reverse(); 290 | let odd = true; 291 | let sum = 0; 292 | 293 | for (let i = 0; i < digits.length; i++) { 294 | let digit = parseInt(digits[i], 10); 295 | if ((odd = !odd)) { 296 | digit *= 2; 297 | } 298 | if (digit > 9) { 299 | digit -= 9; 300 | } 301 | sum += digit; 302 | } 303 | 304 | return sum % 10 === 0; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/directives/credit-card-format.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; 2 | import { CreditCardFormatDirective } from './credit-card-format.directive'; 3 | import { Component, DebugElement } from '@angular/core'; 4 | import { By } from '@angular/platform-browser'; 5 | 6 | const KEY_MAP = { 7 | ONE: 49, // input `1` 8 | BACKSPACE: 8, 9 | }; 10 | 11 | function createKeyEvent(keyCode: number) { 12 | return {keyCode, which: keyCode, preventDefault: jest.fn()}; 13 | } 14 | 15 | function triggerKeyEvent(input: DebugElement, eventName: string, keyCode: number) { 16 | input.triggerEventHandler(eventName, createKeyEvent(keyCode)); 17 | } 18 | 19 | 20 | describe('Directive: CreditCardFormat', () => { 21 | 22 | describe('general cases', () => { 23 | @Component({ 24 | template: ``, 25 | }) 26 | class TestCreditCardFormatComponent {} 27 | 28 | let fixture: ComponentFixture; 29 | let inputEl: DebugElement; 30 | beforeEach(() => { 31 | TestBed.configureTestingModule({ 32 | declarations: [TestCreditCardFormatComponent], 33 | imports: [CreditCardFormatDirective] 34 | }); 35 | fixture = TestBed.createComponent(TestCreditCardFormatComponent); 36 | inputEl = fixture.debugElement.query(By.css('input')); 37 | }); 38 | 39 | it('formats card number tick by tick', fakeAsync(() => { 40 | 41 | inputEl.nativeElement.value = '4111 1111'; 42 | triggerKeyEvent(inputEl, 'keydown', KEY_MAP.ONE); 43 | 44 | fixture.detectChanges(); 45 | tick(10); 46 | expect(inputEl.nativeElement.value).toBe('4111 1111'); 47 | 48 | triggerKeyEvent(inputEl, 'keypress', KEY_MAP.ONE); 49 | fixture.detectChanges(); 50 | tick(10); 51 | expect(inputEl.nativeElement.value).toBe('4111 1111'); 52 | 53 | // the value is changed here by the browser as default behavior 54 | inputEl.nativeElement.value = '4111 11111'; 55 | 56 | inputEl.nativeElement.focus(); 57 | inputEl.triggerEventHandler('input', null); 58 | fixture.detectChanges(); 59 | tick(10); 60 | expect(inputEl.nativeElement.value).toBe('4111 1111 1'); 61 | triggerKeyEvent(inputEl, 'keyup', KEY_MAP.ONE); 62 | 63 | fixture.detectChanges(); 64 | tick(10); 65 | expect(inputEl.nativeElement.value).toBe('4111 1111 1'); 66 | })); 67 | 68 | it('formats card number one tick', fakeAsync(() => { 69 | 70 | inputEl.nativeElement.value = '4111 1111'; 71 | inputEl.triggerEventHandler('keydown', {keyCode: KEY_MAP.ONE, which: KEY_MAP.ONE}); 72 | fixture.detectChanges(); 73 | expect(inputEl.nativeElement.value).toBe('4111 1111'); 74 | 75 | inputEl.triggerEventHandler('keypress', {keyCode: KEY_MAP.ONE, which: KEY_MAP.ONE}); 76 | fixture.detectChanges(); 77 | expect(inputEl.nativeElement.value).toBe('4111 1111'); 78 | 79 | // the value is changed here by the browser as default behavior 80 | inputEl.nativeElement.value = '4111 11111'; 81 | 82 | inputEl.triggerEventHandler('input', null); 83 | fixture.detectChanges(); 84 | expect(inputEl.nativeElement.value).toBe('4111 1111 1'); 85 | })); 86 | 87 | it('deletes from middle of value', fakeAsync(() => { 88 | 89 | inputEl.nativeElement.value = '4111 1111 111'; 90 | inputEl.nativeElement.selectionStart = 5; 91 | inputEl.nativeElement.selectionEnd = 5; 92 | inputEl.nativeElement.focus(); 93 | 94 | const event = createKeyEvent(KEY_MAP.BACKSPACE); 95 | 96 | inputEl.triggerEventHandler('keydown', event); 97 | fixture.detectChanges(); 98 | tick(10); 99 | expect(inputEl.nativeElement.value).toBe('4111 1111 11'); 100 | expect(inputEl.nativeElement.selectionStart).toBe(3); 101 | expect(inputEl.nativeElement.selectionEnd).toBe(3); 102 | expect(event.preventDefault).toBeCalled(); 103 | 104 | })); 105 | 106 | it('deletes from beginning of value', fakeAsync(() => { 107 | 108 | inputEl.nativeElement.value = '5 411 1111'; 109 | inputEl.nativeElement.selectionStart = 2; 110 | inputEl.nativeElement.selectionEnd = 2; 111 | inputEl.nativeElement.focus(); 112 | 113 | const event = createKeyEvent(KEY_MAP.BACKSPACE); 114 | 115 | inputEl.triggerEventHandler('keydown', event); 116 | fixture.detectChanges(); 117 | tick(10); 118 | expect(inputEl.nativeElement.value).toBe('4111 111'); 119 | expect(inputEl.nativeElement.selectionStart).toBe(0); 120 | expect(inputEl.nativeElement.selectionEnd).toBe(0); 121 | expect(event.preventDefault).toBeCalled(); 122 | 123 | })); 124 | 125 | it('does not modify deleting from end of value', fakeAsync(() => { 126 | 127 | inputEl.nativeElement.value = '4111 1111 111'; 128 | inputEl.nativeElement.selectionStart = 13; 129 | inputEl.nativeElement.selectionEnd = 13; 130 | inputEl.nativeElement.focus(); 131 | 132 | const event = createKeyEvent(KEY_MAP.BACKSPACE); 133 | inputEl.triggerEventHandler('keydown', event); 134 | fixture.detectChanges(); 135 | tick(10); 136 | expect(inputEl.nativeElement.value).toBe('4111 1111 111'); 137 | expect(inputEl.nativeElement.selectionStart).toBe(13); 138 | expect(inputEl.nativeElement.selectionEnd).toBe(13); 139 | expect(event.preventDefault).not.toBeCalled(); 140 | 141 | })); 142 | }); 143 | 144 | describe('exportAs cases', () => { 145 | @Component({ 146 | template: ` 147 | {{ccNumber.resolvedScheme$ | async}}`, 148 | }) 149 | class TestCreditCardFormatComponent {} 150 | 151 | let fixture: ComponentFixture; 152 | let inputEl: DebugElement; 153 | 154 | beforeEach(() => { 155 | TestBed.configureTestingModule({ 156 | declarations: [TestCreditCardFormatComponent], 157 | imports: [CreditCardFormatDirective], 158 | }); 159 | fixture = TestBed.createComponent(TestCreditCardFormatComponent); 160 | inputEl = fixture.debugElement.query(By.css('input')); 161 | }); 162 | 163 | it('should provide resolved scheme via exportAs', () => { 164 | (inputEl.nativeElement as HTMLInputElement).value = '4111111111111111'; 165 | inputEl.triggerEventHandler('input', null); 166 | fixture.detectChanges(); 167 | 168 | const span: HTMLSpanElement = fixture.debugElement.query(By.css('.scheme')).nativeElement; 169 | expect(span.textContent).toBe('visa'); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/directives/credit-card-format.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Optional, Self } from '@angular/core'; 2 | import { CreditCard } from '../credit-card'; 3 | import { NgControl } from '@angular/forms'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | @Directive({ 7 | selector: '[ccNumber]', 8 | exportAs: 'ccNumber', 9 | standalone: true, 10 | }) 11 | export class CreditCardFormatDirective { 12 | private target: HTMLInputElement; 13 | private cards = CreditCard.cards(); 14 | 15 | public resolvedScheme$ = new BehaviorSubject('unknown'); 16 | 17 | constructor( 18 | private el: ElementRef, 19 | @Self() @Optional() private control: NgControl, 20 | ) { 21 | this.target = this.el.nativeElement; 22 | } 23 | 24 | /** 25 | * Updates the value to target element, or FormControl if exists. 26 | * @param value New input value. 27 | */ 28 | private updateValue(value: string) { 29 | if (this.control) { 30 | this.control.control.setValue(value); 31 | } else { 32 | this.target.value = value; 33 | } 34 | } 35 | 36 | @HostListener('keypress', ['$event']) 37 | public onKeypress(e: KeyboardEvent) { 38 | if (CreditCard.restrictNumeric(e)) { 39 | if (CreditCard.isCardNumber(e.which, this.target)) { 40 | this.formatCardNumber(e); 41 | } 42 | } else { 43 | e.preventDefault(); 44 | } 45 | } 46 | 47 | @HostListener('keydown', ['$event']) 48 | public onKeydown(e: KeyboardEvent) { 49 | this.formatBackCardNumber(e); 50 | this.reFormatCardNumber(); 51 | } 52 | 53 | @HostListener('keyup') 54 | public onKeyup() { 55 | this.setCardType(); 56 | } 57 | 58 | @HostListener('paste') 59 | public onPaste() { 60 | this.reFormatCardNumber(); 61 | } 62 | 63 | @HostListener('change') 64 | public onChange() { 65 | this.reFormatCardNumber(); 66 | } 67 | 68 | @HostListener('input') 69 | public onInput() { 70 | this.reFormatCardNumber(); 71 | this.setCardType(); 72 | } 73 | 74 | private formatCardNumber(e: KeyboardEvent) { 75 | const digit = String.fromCharCode(e.which); 76 | if (!/^\d+$/.test(digit)) { 77 | return; 78 | } 79 | 80 | const value = this.target.value; 81 | const card = CreditCard.cardFromNumber(value + digit); 82 | const length = (value.replace(/\D/g, '') + digit).length; 83 | const upperLength = card ? card.length[card.length.length - 1] : 19; 84 | 85 | if (length >= upperLength) { 86 | return; 87 | } 88 | } 89 | 90 | private formatBackCardNumber(e: KeyboardEvent) { 91 | const value = this.target.value; 92 | const selStart = this.target.selectionStart; 93 | 94 | if (e.which !== 8) { 95 | return; 96 | } 97 | 98 | if (selStart != null 99 | && selStart === this.target.selectionEnd 100 | && selStart > 0 101 | && selStart !== value.length 102 | && value[selStart - 1] === ' ') { 103 | e.preventDefault(); 104 | if (selStart <= 2) { 105 | this.updateValue(value.slice(selStart)); 106 | this.target.selectionStart = 0; 107 | this.target.selectionEnd = 0; 108 | } else { 109 | this.updateValue(value.slice(0, selStart - 2) + value.slice(selStart)); 110 | this.target.selectionStart = selStart - 2; 111 | this.target.selectionEnd = selStart - 2; 112 | } 113 | } 114 | } 115 | 116 | private setCardType() { 117 | const cardType = CreditCard.cardType(this.target.value) || 'unknown'; 118 | 119 | this.resolvedScheme$.next(cardType); 120 | 121 | if (!this.target.classList.contains(cardType)) { 122 | this.cards.forEach((card) => { 123 | this.target.classList.remove(card.type); 124 | }); 125 | 126 | this.target.classList.remove('unknown'); 127 | this.target.classList.add(cardType); 128 | this.target.classList.toggle('identified', cardType !== 'unknown'); 129 | } 130 | } 131 | 132 | private reFormatCardNumber() { 133 | const value = CreditCard.formatCardNumber( 134 | CreditCard.replaceFullWidthChars(this.target.value), 135 | ); 136 | const oldValue = this.target.value; 137 | if (value !== oldValue) { 138 | this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(value, this.target, (safeVal => { 139 | this.updateValue(safeVal); 140 | })); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/directives/cvc-format.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Optional, Self } from '@angular/core'; 2 | import { CreditCard } from '../credit-card'; 3 | import { NgControl } from '@angular/forms'; 4 | 5 | @Directive({ 6 | selector: '[ccCVC]', 7 | standalone: true, 8 | }) 9 | export class CvcFormatDirective { 10 | private target: HTMLInputElement; 11 | 12 | constructor( 13 | private el: ElementRef, 14 | @Self() @Optional() private control: NgControl, 15 | ) { 16 | this.target = this.el.nativeElement; 17 | } 18 | 19 | /** 20 | * Updates the value to target element, or FormControl if exists. 21 | * @param value New input value. 22 | */ 23 | private updateValue(value: string) { 24 | if (this.control) { 25 | this.control.control.setValue(value); 26 | } else { 27 | this.target.value = value; 28 | } 29 | } 30 | 31 | @HostListener('keypress', ['$event']) 32 | public onKeypress(e: KeyboardEvent) { 33 | if (!CreditCard.restrictNumeric(e) && !CreditCard.restrictCvc(e.which, this.target)) { 34 | e.preventDefault(); 35 | } 36 | } 37 | 38 | @HostListener('paste') 39 | @HostListener('change') 40 | @HostListener('input') 41 | public reformatCvc() { 42 | const val = CreditCard.replaceFullWidthChars(this.target.value) 43 | .replace(/\D/g, '') 44 | .slice(0, 4); 45 | const oldVal = this.target.value; 46 | if (val !== oldVal) { 47 | this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => { 48 | this.updateValue(safeVal); 49 | })); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/directives/expiry-format.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Optional, Self } from '@angular/core'; 2 | import { CreditCard } from '../credit-card'; 3 | import { NgControl } from '@angular/forms'; 4 | 5 | @Directive({ 6 | selector: '[ccExp]', 7 | standalone: true, 8 | }) 9 | export class ExpiryFormatDirective { 10 | private target: HTMLInputElement; 11 | 12 | constructor( 13 | private el: ElementRef, 14 | @Self() @Optional() private control: NgControl, 15 | ) { 16 | this.target = this.el.nativeElement; 17 | } 18 | 19 | /** 20 | * Updates the value to target element, or FormControl if exists. 21 | * @param value New input value. 22 | */ 23 | private updateValue(value: string) { 24 | if (this.control) { 25 | this.control.control.setValue(value); 26 | } else { 27 | this.target.value = value; 28 | } 29 | } 30 | 31 | @HostListener('keypress', ['$event']) 32 | public onKeypress(e: KeyboardEvent) { 33 | if (CreditCard.restrictNumeric(e)) { 34 | if (CreditCard.restrictExpiry(e.which, this.target)) { 35 | this.formatExpiry(e); 36 | this.formatForwardSlashAndSpace(e); 37 | this.formatForwardExpiry(e); 38 | } 39 | } else { 40 | e.preventDefault(); 41 | return false; 42 | } 43 | } 44 | 45 | @HostListener('keydown', ['$event']) 46 | public onKeydown(e: KeyboardEvent) { 47 | if (CreditCard.restrictNumeric(e) && CreditCard.restrictExpiry(e.which, this.target)) { 48 | this.formatBackExpiry(e); 49 | } 50 | } 51 | 52 | @HostListener('change') 53 | public onChange() { 54 | this.reformatExpiry(); 55 | } 56 | 57 | @HostListener('input') 58 | public onInput() { 59 | this.reformatExpiry(); 60 | } 61 | 62 | private formatExpiry(e: KeyboardEvent) { 63 | const digit = String.fromCharCode(e.which); 64 | const val = `${this.target.value}${digit}`; 65 | 66 | if (!/^\d+$/.test(digit)) { 67 | return; 68 | } 69 | 70 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 71 | e.preventDefault(); 72 | this.updateValue(`0${val} / `); 73 | } else if (/^\d\d$/.test(val)) { 74 | e.preventDefault(); 75 | const m1 = parseInt(val[0], 10); 76 | const m2 = parseInt(val[1], 10); 77 | if (m2 > 2 && m1 !== 0) { 78 | this.updateValue(`0${m1} / ${m2}`); 79 | } else { 80 | this.updateValue(`${val} / `); 81 | } 82 | 83 | } 84 | } 85 | 86 | private formatForwardSlashAndSpace(e: KeyboardEvent) { 87 | const which = String.fromCharCode(e.which); 88 | const val = this.target.value; 89 | 90 | if (!(which === '/' || which === ' ')) { 91 | return false; 92 | } 93 | if (/^\d$/.test(val) && val !== '0') { 94 | this.updateValue(`0${val} / `); 95 | } 96 | } 97 | 98 | private formatForwardExpiry(e: KeyboardEvent) { 99 | const digit = String.fromCharCode(e.which); 100 | const val = this.target.value; 101 | 102 | if (!/^\d+$/.test(digit) && /^\d\d$/.test(val)) { 103 | this.updateValue(this.target.value = `${val} / `); 104 | } 105 | } 106 | 107 | private formatBackExpiry(e: KeyboardEvent) { 108 | const val = this.target.valueOf as unknown as string; 109 | 110 | if (e.which !== 8) { 111 | return; 112 | } 113 | if ((this.target.selectionStart != null) && this.target.selectionStart !== val.length) { 114 | return; 115 | } 116 | if (/\d\s\/\s$/.test(val)) { 117 | e.preventDefault(); 118 | this.updateValue(val.replace(/\d\s\/\s$/, '')); 119 | } 120 | } 121 | 122 | private reformatExpiry() { 123 | const val = CreditCard.formatExpiry( 124 | CreditCard.replaceFullWidthChars(this.target.value), 125 | ); 126 | 127 | const oldVal = this.target.value; 128 | if (val !== oldVal) { 129 | this.target.selectionStart = this.target.selectionEnd = CreditCard.safeVal(val, this.target, (safeVal => { 130 | this.updateValue(safeVal); 131 | })); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'; 2 | import { CreditCard } from './credit-card'; 3 | 4 | export class CreditCardValidators { 5 | public static validateCCNumber(control: AbstractControl): ValidationErrors | null { 6 | if (Validators.required(control) !== undefined && Validators.required(control) !== null) { 7 | return {ccNumber: true}; 8 | } 9 | 10 | const num = control.value.toString().replace(/\s+|-/g, ''); 11 | 12 | if (!/^\d+$/.test(num)) { 13 | return {ccNumber: true}; 14 | } 15 | 16 | const card = CreditCard.cardFromNumber(num); 17 | 18 | if (!card) { 19 | return {ccNumber: true}; 20 | } 21 | 22 | if (card.length.includes(num.length) && (card.luhn === false || CreditCard.luhnCheck(num))) { 23 | return null; 24 | } 25 | 26 | const upperlength = card.length[card.length.length - 1]; 27 | if (num.length > upperlength) { 28 | const registeredNum = num.substring(0, upperlength); 29 | if (CreditCard.luhnCheck(registeredNum)) { 30 | return null; 31 | } 32 | } 33 | 34 | return {ccNumber: true}; 35 | } 36 | 37 | public static validateExpDate(control: AbstractControl): ValidationErrors | null { 38 | if (Validators.required(control) !== undefined && Validators.required(control) !== null) { 39 | return {expDate: true}; 40 | } 41 | 42 | if (typeof control.value !== 'undefined' && control.value.length >= 5) { 43 | let [month, year] = control.value.split(/[\s/]+/, 2); 44 | 45 | if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { 46 | const prefix = new Date().getFullYear().toString().slice(0, 2); 47 | year = prefix + year; 48 | } 49 | 50 | month = parseInt(month, 10).toString(); 51 | year = parseInt(year, 10).toString(); 52 | 53 | if (/^\d+$/.test(month) && /^\d+$/.test(year) && (month >= 1 && month <= 12)) { 54 | const expiry = new Date(year, month); 55 | const currentTime = new Date(); 56 | expiry.setMonth(expiry.getMonth() - 1); 57 | expiry.setMonth(expiry.getMonth() + 1, 1); 58 | 59 | if (expiry > currentTime) { 60 | return null; 61 | } 62 | } 63 | } 64 | 65 | return {expDate: true}; 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /projects/angular-cc-library/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular-cc-library 3 | */ 4 | 5 | export * from './lib/validators'; 6 | export * from './lib/credit-card'; 7 | 8 | export * from './lib/directives/credit-card-format.directive'; 9 | export * from './lib/directives/cvc-format.directive'; 10 | export * from './lib/directives/expiry-format.directive'; 11 | 12 | export * from './lib/credit-card-directives.module'; 13 | -------------------------------------------------------------------------------- /projects/angular-cc-library/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "declaration": true, 7 | "inlineSources": true, 8 | "types": [], 9 | "lib": [ 10 | "dom", 11 | "es2018" 12 | ] 13 | }, 14 | "angularCompilerOptions": { 15 | "skipTemplateCodegen": true, 16 | "strictMetadataEmit": true, 17 | "enableResourceInlining": true 18 | }, 19 | "exclude": [ 20 | "src/test.ts", 21 | "**/*.spec.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /projects/angular-cc-library/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/angular-cc-library/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "emitDecoratorMetadata": true, 6 | "outDir": "../../out-tsc/spec", 7 | "types": [ 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/example/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": [ 4 | "!**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": [ 32 | "*.html" 33 | ], 34 | "rules": {} 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /projects/example/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
5 |

A general purpose library for building credit card forms, validating inputs and formatting numbers.

6 |
7 | 12 | 20 |
21 | 22 |
23 | 24 | 33 |
34 | 35 |
36 | 37 | 46 |
47 | 48 |
49 | @if(!demoForm.controls.creditCard.valid && demoForm.controls.creditCard.dirty){ 50 |
Credit Card is invalid
51 | } 52 | @if(!demoForm.controls.expDate.valid && demoForm.controls.expDate.dirty){ 53 |
Expiration Date is required
54 | } 55 | @if(!demoForm.controls.cvc.valid && demoForm.controls.cvc.dirty){ 56 |
CVC is required
57 | } 58 |
59 |
60 | 61 | -------------------------------------------------------------------------------- /projects/example/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { AsyncPipe } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { Validators, FormGroup, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { CreditCardValidators, CreditCard, CreditCardDirectivesModule } from 'angular-cc-library'; 5 | import { defer } from 'rxjs'; 6 | import { map } from 'rxjs/operators'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | standalone: true, 12 | imports: [FormsModule, ReactiveFormsModule, AsyncPipe, CreditCardDirectivesModule] 13 | }) 14 | export class AppComponent { 15 | public demoForm = this.fb.group({ 16 | creditCard: ['', [CreditCardValidators.validateCCNumber]], 17 | expDate: ['', [CreditCardValidators.validateExpDate]], 18 | cvc: ['', [Validators.required, Validators.minLength(3), Validators.maxLength(4)]], 19 | }); 20 | 21 | public submitted = false; 22 | 23 | public type$ = defer(() => this.demoForm.get('creditCard').valueChanges) 24 | .pipe(map((num: string) => CreditCard.cardType(num))); 25 | 26 | constructor(private fb: FormBuilder) {} 27 | 28 | public goToNextField(controlName: string, nextField: HTMLInputElement) { 29 | if (this.demoForm.get(controlName)?.valid) { 30 | nextField.focus(); 31 | } 32 | } 33 | 34 | public onSubmit(demoForm: FormGroup) { 35 | this.submitted = true; 36 | console.log(demoForm.value); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/example/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timofei-iatsenko/angular-cc-library/13bdb566f1e7a2adbd085868653378f4c1df8b7b/projects/example/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/example/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/example/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/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/example/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timofei-iatsenko/angular-cc-library/13bdb566f1e7a2adbd085868653378f4c1df8b7b/projects/example/src/favicon.ico -------------------------------------------------------------------------------- /projects/example/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /projects/example/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { environment } from './environments/environment'; 3 | import { AppComponent } from './app/app.component'; 4 | import { bootstrapApplication } from '@angular/platform-browser'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | bootstrapApplication(AppComponent) 11 | .catch(err => console.error(err)); 12 | -------------------------------------------------------------------------------- /projects/example/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 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/example/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.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/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": false, 8 | "noImplicitThis": true, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "strictNullChecks": false, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "module": "es2020", 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "lib": [ 22 | "es2018", 23 | "dom" 24 | ], 25 | "paths": { 26 | "angular-cc-library/*": [ 27 | "../angular-cc-library/src/*" 28 | ], 29 | "angular-cc-library": [ 30 | "dist/angular-cc-library/angular-cc-library", 31 | "dist/angular-cc-library", 32 | "projects/angular-cc-library/src/public-api.ts" 33 | ] 34 | }, 35 | "useDefineForClassFields": false 36 | }, 37 | "angularCompilerOptions": { 38 | "fullTemplateTypeCheck": true, 39 | "strictInjectionParameters": true 40 | } 41 | } 42 | --------------------------------------------------------------------------------