├── .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 |
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 |
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 |
--------------------------------------------------------------------------------