├── .browserslistrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── forms │ │ ├── components │ │ │ ├── delivery-page │ │ │ │ ├── delivery-page.component.css │ │ │ │ ├── delivery-page.component.html │ │ │ │ └── delivery-page.component.ts │ │ │ ├── forms-home │ │ │ │ ├── forms-home.component.css │ │ │ │ ├── forms-home.component.html │ │ │ │ └── forms-home.component.ts │ │ │ ├── payment-page │ │ │ │ ├── payment-page.component.css │ │ │ │ ├── payment-page.component.html │ │ │ │ └── payment-page.component.ts │ │ │ ├── review-page │ │ │ │ ├── review-page.component.css │ │ │ │ ├── review-page.component.html │ │ │ │ └── review-page.component.ts │ │ │ └── shippping-page │ │ │ │ ├── shipping-page.component.css │ │ │ │ ├── shipping-page.component.html │ │ │ │ └── shipping-page.component.ts │ │ ├── forms-routing.module.ts │ │ ├── forms.module.ts │ │ ├── models │ │ │ ├── delivery-page-form-value.interface.ts │ │ │ ├── delivery-page-form-value.mock.ts │ │ │ ├── forms-state-model-init.const.ts │ │ │ ├── froms-state-model.interface.ts │ │ │ ├── payment-page-form-value.interface.ts │ │ │ ├── shipping-method.interface.ts │ │ │ ├── shipping-methods.const.ts │ │ │ └── shipping-page-form-value.interface.ts │ │ ├── services │ │ │ ├── can-activate-forms.ts │ │ │ ├── delivery-page-form.validator.spec.ts │ │ │ └── delivery-page-form.validator.ts │ │ └── store │ │ │ ├── forms.selectors.ts │ │ │ └── forms.state.ts │ ├── models │ │ ├── app-state-model.interface.ts │ │ ├── forms-state-init.const.ts │ │ ├── forms-state.interface.ts │ │ └── route-path.enum.ts │ └── shared │ │ ├── address-form │ │ ├── address-form-value.interface.ts │ │ ├── address-form.component.html │ │ └── address-form.component.ts │ │ ├── form-group-error-state-matcher.ts │ │ ├── material.module.ts │ │ ├── regexes.const.ts │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ ├── i18n │ │ └── en.json │ └── icon.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── icon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css └── test.ts ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "createDefaultProgram": true 17 | }, 18 | "extends": [ 19 | "plugin:@angular-eslint/recommended", 20 | "plugin:@angular-eslint/template/process-inline-templates" 21 | ], 22 | "rules": { 23 | "@angular-eslint/directive-selector": [ 24 | "error", 25 | { 26 | "type": "attribute", 27 | "prefix": "afn", 28 | "style": "camelCase" 29 | } 30 | ], 31 | "@angular-eslint/component-selector": [ 32 | "error", 33 | { 34 | "type": "element", 35 | "prefix": "afn", 36 | "style": "kebab-case" 37 | } 38 | ] 39 | } 40 | }, 41 | { 42 | "files": [ 43 | "*.html" 44 | ], 45 | "extends": [ 46 | "plugin:@angular-eslint/template/recommended" 47 | ], 48 | "rules": {} 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "printWidth": 140 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Forms Example with Ngxs 2 | 3 | Blog: https://medium.com/swlh/how-to-create-a-complex-form-in-angular-bdfaee0464d3 4 | 5 | ## Commands 6 | 7 | - run locally: `npm run start` and it would be served on http://localhost:4200/angular-form-ngxs 8 | - deploy to github pages: `npm run build && npm run deploy` 9 | - lint: `npm run lint` and `npm run lint-fix` 10 | 11 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4. 12 | 13 | ## Libraries 14 | 15 | - Component Library: [Angular Material](https://material.angular.io/) 16 | - State Management: [NGXS](https://www.ngxs.io/) 17 | - CSS: [tachyons](https://tachyons.io/) 18 | 19 | ## Related Links 20 | 21 | - github page: https://xiongemi.github.io/angular-form-ngxs 22 | - medium post: https://medium.com/@emilyxiong/how-to-create-a-complex-form-in-angular-bdfaee0464d3 23 | 24 | ## Development server 25 | 26 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/angular-form-ngxs`. The app will automatically reload if you change any of the source files. 27 | 28 | ## Code scaffolding 29 | 30 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 31 | 32 | ## Build 33 | 34 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 35 | 36 | ## Running unit tests 37 | 38 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 39 | 40 | ## Running end-to-end tests 41 | 42 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 43 | 44 | ## Further help 45 | 46 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 47 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-forms-ngxs": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "afn", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/angular-forms-ngxs", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | "src/icon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [], 29 | "vendorChunk": true, 30 | "extractLicenses": false, 31 | "buildOptimizer": false, 32 | "sourceMap": true, 33 | "optimization": false, 34 | "namedChunks": true 35 | }, 36 | "configurations": { 37 | "production": { 38 | "fileReplacements": [ 39 | { 40 | "replace": "src/environments/environment.ts", 41 | "with": "src/environments/environment.prod.ts" 42 | } 43 | ], 44 | "optimization": true, 45 | "outputHashing": "all", 46 | "sourceMap": false, 47 | "namedChunks": false, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true, 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "2mb", 55 | "maximumError": "5mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "6kb", 60 | "maximumError": "10kb" 61 | } 62 | ] 63 | } 64 | }, 65 | "defaultConfiguration": "" 66 | }, 67 | "serve": { 68 | "builder": "@angular-devkit/build-angular:dev-server", 69 | "options": { 70 | "browserTarget": "angular-forms-ngxs:build" 71 | }, 72 | "configurations": { 73 | "production": { 74 | "browserTarget": "angular-forms-ngxs:build:production" 75 | } 76 | } 77 | }, 78 | "extract-i18n": { 79 | "builder": "@angular-devkit/build-angular:extract-i18n", 80 | "options": { 81 | "browserTarget": "angular-forms-ngxs:build" 82 | } 83 | }, 84 | "test": { 85 | "builder": "@angular-devkit/build-angular:karma", 86 | "options": { 87 | "main": "src/test.ts", 88 | "polyfills": "src/polyfills.ts", 89 | "tsConfig": "tsconfig.spec.json", 90 | "karmaConfig": "karma.conf.js", 91 | "assets": [ 92 | "src/icon.ico", 93 | "src/assets" 94 | ], 95 | "styles": [ 96 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 97 | "src/styles.css" 98 | ], 99 | "scripts": [] 100 | } 101 | }, 102 | "e2e": { 103 | "builder": "@angular-devkit/build-angular:protractor", 104 | "options": { 105 | "protractorConfig": "e2e/protractor.conf.js", 106 | "devServerTarget": "angular-forms-ngxs:serve" 107 | }, 108 | "configurations": { 109 | "production": { 110 | "devServerTarget": "angular-forms-ngxs:serve:production" 111 | } 112 | } 113 | }, 114 | "deploy": { 115 | "builder": "angular-cli-ghpages:deploy", 116 | "options": { 117 | "baseHref": "/angular-form-ngxs/", 118 | "name": "Emily Xiong", 119 | "email": "xiongemi@gmail.com" 120 | } 121 | }, 122 | "lint": { 123 | "builder": "@angular-eslint/builder:lint", 124 | "options": { 125 | "lintFilePatterns": [ 126 | "src/**/*.ts", 127 | "src/**/*.html" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "angular-forms-ngxs", 135 | "cli": { 136 | "analytics": "0abf01a7-93bc-4bbd-a242-c12c962270d0", 137 | "defaultCollection": "@angular-eslint/schematics" 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular-forms-ngxs'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-forms-ngxs", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --serve-path=/angular-form-ngxs/", 7 | "build": "ng build --baseHref=/angular-form-ngxs/", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "lint-fix": "ng lint --fix", 11 | "deploy": "ng deploy" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~13.2.5", 16 | "@angular/cdk": "~13.2.5", 17 | "@angular/common": "~13.2.5", 18 | "@angular/compiler": "~13.2.5", 19 | "@angular/core": "~13.2.5", 20 | "@angular/forms": "~13.2.5", 21 | "@angular/material": "^13.2.5", 22 | "@angular/platform-browser": "~13.2.5", 23 | "@angular/platform-browser-dynamic": "~13.2.5", 24 | "@angular/router": "~13.2.5", 25 | "@ngx-translate/core": "^14.0.0", 26 | "@ngx-translate/http-loader": "^7.0.0", 27 | "@ngxs/form-plugin": "^3.7.3", 28 | "@ngxs/logger-plugin": "^3.7.3", 29 | "@ngxs/storage-plugin": "^3.7.3", 30 | "@ngxs/store": "^3.7.3", 31 | "ngx-mask": "^13.1.1", 32 | "ngxs-reset-plugin": "^2.0.0", 33 | "prettier": "^2.0.5", 34 | "rxjs": "^7.5.4", 35 | "tslib": "^2.0.0", 36 | "zone.js": "~0.11.4" 37 | }, 38 | "devDependencies": { 39 | "@angular-devkit/build-angular": "~13.2.5", 40 | "@angular-eslint/builder": "13.1.0", 41 | "@angular-eslint/eslint-plugin": "13.1.0", 42 | "@angular-eslint/eslint-plugin-template": "13.1.0", 43 | "@angular-eslint/schematics": "13.1.0", 44 | "@angular-eslint/template-parser": "13.1.0", 45 | "@angular/cli": "~13.2.5", 46 | "@angular/compiler-cli": "~13.2.5", 47 | "@angular/language-service": "~13.2.5", 48 | "@ngxs/devtools-plugin": "^3.7.3", 49 | "@types/jasmine": "~3.6.0", 50 | "@types/jasminewd2": "~2.0.3", 51 | "@types/node": "^12.11.1", 52 | "@typescript-eslint/eslint-plugin": "5.11.0", 53 | "@typescript-eslint/parser": "5.11.0", 54 | "angular-cli-ghpages": "^1.0.0", 55 | "codelyzer": "^6.0.0", 56 | "date-fns": "^2.28.0", 57 | "eslint": "^8.2.0", 58 | "jasmine-core": "~3.6.0", 59 | "jasmine-spec-reporter": "~5.0.0", 60 | "karma": "~6.3.17", 61 | "karma-chrome-launcher": "~3.1.0", 62 | "karma-coverage-istanbul-reporter": "~3.0.2", 63 | "karma-jasmine": "~4.0.0", 64 | "karma-jasmine-html-reporter": "^1.5.0", 65 | "tachyons": "^4.12.0", 66 | "ts-node": "~7.0.0", 67 | "tslint": "~6.1.0", 68 | "typescript": "~4.5.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: '', 7 | loadChildren: () => import('./forms/forms.module').then((m) => m.FormsModule) 8 | } 9 | ]; 10 | 11 | @NgModule({ 12 | imports: [RouterModule.forRoot(routes)], 13 | exports: [RouterModule] 14 | }) 15 | export class AppRoutingModule {} 16 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiongemi/angular-form-ngxs/56c4018052e70aeab744b3e365d2dfbe0e898a4e/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'afn-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 6 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 7 | import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; 8 | import { NgxsFormPluginModule } from '@ngxs/form-plugin'; 9 | import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; 10 | import { NgxsStoragePluginModule } from '@ngxs/storage-plugin'; 11 | import { NgxsModule } from '@ngxs/store'; 12 | import { NgxMaskModule } from 'ngx-mask'; 13 | import { NgxsResetPluginModule } from 'ngxs-reset-plugin'; 14 | 15 | import { environment } from 'src/environments/environment'; 16 | import { AppRoutingModule } from './app-routing.module'; 17 | import { AppComponent } from './app.component'; 18 | 19 | export function createTranslateLoader(http: HttpClient) { 20 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 21 | } 22 | 23 | @NgModule({ 24 | declarations: [AppComponent], 25 | imports: [ 26 | BrowserModule, 27 | BrowserAnimationsModule, 28 | AppRoutingModule, 29 | HttpClientModule, 30 | TranslateModule.forRoot({ 31 | defaultLanguage: 'en', 32 | loader: { 33 | provide: TranslateLoader, 34 | useFactory: createTranslateLoader, 35 | deps: [HttpClient] 36 | } 37 | }), 38 | NgxMaskModule.forRoot(), 39 | NgxsModule.forRoot([], { 40 | developmentMode: !environment.production 41 | }), 42 | NgxsFormPluginModule.forRoot(), 43 | NgxsResetPluginModule.forRoot(), 44 | NgxsLoggerPluginModule.forRoot({ disabled: environment.production }), 45 | NgxsReduxDevtoolsPluginModule.forRoot({ disabled: environment.production }), 46 | NgxsStoragePluginModule.forRoot({ key: ['forms'] }) 47 | ], 48 | providers: [], 49 | bootstrap: [AppComponent] 50 | }) 51 | export class AppModule {} 52 | -------------------------------------------------------------------------------- /src/app/forms/components/delivery-page/delivery-page.component.css: -------------------------------------------------------------------------------- 1 | .mat-checkbox-layout { 2 | white-space: normal !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/forms/components/delivery-page/delivery-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 | 9 | 10 | {{ 'ERRORS.FORM_ERROR' | translate }} 11 | 12 | 13 | 14 | {{ 'ADDRESS.BILLING_ADDRESS' | translate }} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{ 'DELIVERY.SHIPPING_SAME' | translate }} 22 | 23 | 24 | 25 | {{ 'ADDRESS.SHIPPING_ADDRESS' | translate }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {{ 'DELIVERY.CREATE_ACCOUNT' | translate }} 34 | {{ 'DELIVERY.CHECKOUT_AS_GUEST' | translate }} 35 | 36 | 37 | 38 | 39 | 40 | {{ 'DELIVERY.EMAIL' | translate }} 41 | 42 | 43 | {{ 'ERRORS.REQUIRED' | translate }} 44 | 45 | 46 | {{ 'ERRORS.PATTERN' | translate }} 47 | 48 | 49 | 50 |
51 | 52 | 53 | {{ 'DELIVERY.PASSWORD' | translate }} 54 | 55 | 56 | 57 | 58 | {{ 'ERRORS.REQUIRED' | translate }} 59 | 60 | 61 | {{ 'ERRORS.PATTERN' | translate }} 62 | 63 | 64 | 65 | 66 | 67 | {{ 'DELIVERY.CONFIRM_PASSWORD' | translate }} 68 | 69 | 70 | 77 | 78 | {{ 'ERRORS.REQUIRED' | translate }} 79 | 80 | 81 | {{ 'DELIVERY.PASSWORD_MATCH' | translate }} 82 | 83 | 84 | {{ 'DELIVERY.PASSWORD_HINT' | translate }} 85 |
86 |
87 |
88 | 89 |
90 | 94 |
95 |
96 | -------------------------------------------------------------------------------- /src/app/forms/components/delivery-page/delivery-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { Subscription } from 'rxjs'; 5 | import { filter } from 'rxjs/operators'; 6 | 7 | import { RoutePath } from 'src/app/models/route-path.enum'; 8 | import { FormGroupErrorStateMatcher } from 'src/app/shared/form-group-error-state-matcher'; 9 | import { passwordRegex } from 'src/app/shared/regexes.const'; 10 | import { deliveryPageFromValidator } from '../../services/delivery-page-form.validator'; 11 | 12 | @Component({ 13 | selector: 'afn-delivery-page', 14 | templateUrl: './delivery-page.component.html', 15 | styleUrls: ['./delivery-page.component.css'] 16 | }) 17 | export class DeliveryPageComponent implements OnInit, OnDestroy { 18 | deliveryPageForm = this.fb.group( 19 | { 20 | billingAddress: [null, Validators.required], 21 | isShippingSame: [true, Validators.required], 22 | shippingAddress: [null], 23 | createAccount: [true, Validators.required], 24 | account: this.fb.group({ 25 | email: ['', [Validators.required, Validators.email]], 26 | password: ['', [Validators.pattern(passwordRegex)]], 27 | confirmPassword: [''] 28 | }) 29 | }, 30 | { validator: deliveryPageFromValidator } 31 | ); 32 | 33 | passwordErrorStateMatcher = new FormGroupErrorStateMatcher(['password']); 34 | confirmPassowrdErrorStateMatcher = new FormGroupErrorStateMatcher(['confirmPassword', 'passwordMatch']); 35 | submitted = false; 36 | 37 | private subscription = new Subscription(); 38 | 39 | constructor(private fb: FormBuilder, private router: Router) {} 40 | 41 | ngOnInit() { 42 | this.subscription.add( 43 | this.deliveryPageForm 44 | .get('isShippingSame') 45 | .valueChanges.pipe(filter(Boolean)) 46 | .subscribe(() => { 47 | this.deliveryPageForm.get('shippingAddress').reset(); 48 | }) 49 | ); 50 | 51 | this.subscription.add( 52 | this.deliveryPageForm 53 | .get('createAccount') 54 | .valueChanges.pipe(filter((createAccount) => !createAccount)) 55 | .subscribe(() => { 56 | this.deliveryPageForm.get(['account', 'password']).reset(); 57 | this.deliveryPageForm.get(['account', 'confirmPassword']).reset(); 58 | }) 59 | ); 60 | 61 | this.subscription.add( 62 | this.deliveryPageForm.valueChanges.subscribe(() => { 63 | this.submitted = false; 64 | }) 65 | ); 66 | } 67 | 68 | ngOnDestroy() { 69 | this.subscription.unsubscribe(); 70 | } 71 | 72 | onSubmit() { 73 | this.submitted = true; 74 | this.deliveryPageForm.markAllAsTouched(); 75 | if (this.deliveryPageForm.valid) { 76 | this.router.navigate([RoutePath.shipping]); 77 | } 78 | } 79 | 80 | onReset() { 81 | this.deliveryPageForm.reset({ 82 | billingAddress: {}, 83 | isShippingSame: true, 84 | shippingAddress: {}, 85 | createAccount: true, 86 | account: {} 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/app/forms/components/forms-home/forms-home.component.css: -------------------------------------------------------------------------------- 1 | .mat-tab-links { 2 | justify-content: center; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/forms/components/forms-home/forms-home.component.html: -------------------------------------------------------------------------------- 1 | 26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /src/app/forms/components/forms-home/forms-home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import * as FormsSelectors from '../../store/forms.selectors'; 6 | 7 | @Component({ 8 | selector: 'afn-forms-home', 9 | templateUrl: './forms-home.component.html', 10 | styleUrls: ['./forms-home.component.css'] 11 | }) 12 | export class FormsHomeComponent implements OnInit { 13 | isDeliveryFormValid$: Observable; 14 | isPaymentEnabled$: Observable; 15 | 16 | constructor(private store: Store) {} 17 | 18 | ngOnInit() { 19 | this.isDeliveryFormValid$ = this.store.select(FormsSelectors.isDeliveryFormValid); 20 | this.isPaymentEnabled$ = this.store.select(FormsSelectors.isPaymentEnabled); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/forms/components/payment-page/payment-page.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiongemi/angular-form-ngxs/56c4018052e70aeab744b3e365d2dfbe0e898a4e/src/app/forms/components/payment-page/payment-page.component.css -------------------------------------------------------------------------------- /src/app/forms/components/payment-page/payment-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 | 9 | 10 | {{ 'ERRORS.FORM_ERROR' | translate }} 11 | 12 | 13 | 14 | {{ 'PAYMENT.PAYMENT_METHOD' | translate }} 15 | 16 | 17 |
18 | 19 | {{ 'PAYMENT.CARD_NUMBER' | translate }} 20 | 21 | 22 | {{ 'ERRORS.REQUIRED' | translate }} 23 | 24 | 27 | {{ 'ERRORS.PATTERN' | translate }} 28 | 29 | 30 |
31 | 32 |
33 | 34 | {{ 'PAYMENT.EXPIRY_DATE' | translate }} 35 | 36 | 37 | {{ 'ERRORS.REQUIRED' | translate }} 38 | 39 | 40 | 41 | 42 | {{ 'PAYMENT.CVV' | translate }} 43 | 44 | 45 | {{ 'ERRORS.REQUIRED' | translate }} 46 | 47 | 48 | {{ 'ERRORS.PATTERN' | translate }} 49 | 50 | 51 |
52 |
53 |
54 | 55 |
56 | 60 | 61 | 65 |
66 |
67 | -------------------------------------------------------------------------------- /src/app/forms/components/payment-page/payment-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { format } from 'date-fns'; 5 | import { Subscription } from 'rxjs'; 6 | 7 | import { RoutePath } from 'src/app/models/route-path.enum'; 8 | import { numberOnlyRegex } from 'src/app/shared/regexes.const'; 9 | 10 | const minExpiryDate = format(new Date(), 'yyyy-MM'); 11 | 12 | @Component({ 13 | selector: 'afn-payment-page', 14 | templateUrl: './payment-page.component.html', 15 | styleUrls: ['./payment-page.component.css'] 16 | }) 17 | export class PaymentPageComponent implements OnInit, OnDestroy { 18 | paymentPageForm = this.fb.group({ 19 | cardNumber: [null, [Validators.required, Validators.pattern(numberOnlyRegex)]], 20 | expiryDate: [null, [Validators.required]], 21 | cvv: [null, [Validators.required, Validators.pattern(numberOnlyRegex)]] 22 | }); 23 | 24 | minExpiryDate = minExpiryDate; 25 | submitted = false; 26 | 27 | private subscription = new Subscription(); 28 | 29 | constructor(private fb: FormBuilder, private router: Router) {} 30 | 31 | ngOnInit() { 32 | this.subscription.add( 33 | this.paymentPageForm.valueChanges.subscribe(() => { 34 | this.submitted = false; 35 | }) 36 | ); 37 | } 38 | 39 | ngOnDestroy() { 40 | this.subscription.unsubscribe(); 41 | } 42 | 43 | onSubmit() { 44 | this.submitted = true; 45 | this.paymentPageForm.markAllAsTouched(); 46 | if (this.paymentPageForm.valid) { 47 | this.router.navigate([RoutePath.review]); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/forms/components/review-page/review-page.component.css: -------------------------------------------------------------------------------- 1 | th { 2 | padding: 0.5rem; 3 | text-align: right; 4 | vertical-align: top; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/forms/components/review-page/review-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'COMMON.REVIEW' | translate }}

3 | 4 | 5 |

6 | {{ 'TABS.DELIVERY' | translate }} 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 |
{{ 'ADDRESS.BILLING_ADDRESS' | translate }}:
{{ 'ADDRESS.SHIPPING_ADDRESS' | translate }}: 16 | 17 | {{ 'DELIVERY.SHIPPING_SAME' | translate }} 18 | 19 |
23 |
{{ 'DELIVERY.EMAIL' | translate }}:{{ formsValues?.deliveryForm?.model?.account?.email }}
30 | 31 | 32 |

33 | {{ 'TABS.SHIPPING' | translate }} 34 |

35 | 36 |
37 | 38 | {{ shippingMethod?.price ? (shippingMethod?.price | currency) : '' }} 39 | {{ shippingMethod?.type }}: 40 | 41 | {{ 'SHIPPING.GET_IT_BY' | translate }} 42 | {{ shippingMethod?.arrival?.min | date: 'E, MMM d' }} 43 | {{ shippingMethod?.arrival?.min ? '-' : '' }} 44 | {{ shippingMethod?.arrival?.max | date: 'E, MMM d' }} 45 |
46 | 47 | 48 |

{{ 'SHIPPING.IS_A_GIFT' | translate }}

49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 77 | 78 |
{{ 'SHIPPING.INCLUDE_GIFT_RECEIPT' | translate }}: 53 | 54 | check 55 | 56 | 57 | close 58 | 59 |
{{ 'SHIPPING.RECIPIENT_NAME' | translate }}: 64 | {{ formsValues?.shippingForm?.model?.giftOptions?.name || '/' }} 65 |
{{ 'SHIPPING.CUSTOME_MESSAGE' | translate }}:{{ formsValues?.shippingForm?.model?.giftOptions?.message || '/' }}
{{ 'SHIPPING.GIFT_WRAP' | translate }}: 74 | check 75 | close 76 |
79 |
80 | 81 | 82 |

83 | {{ 'TABS.PAYMENT' | translate }} 84 |

85 |
86 | {{ 'PAYMENT.CARD_NUMBER' | translate }}: 87 | {{ maskCardNumber(formsValues?.paymentForm?.model?.cardNumber) }} 88 |
89 |
90 | 91 |
92 | 96 |
97 | -------------------------------------------------------------------------------- /src/app/forms/components/review-page/review-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Store } from '@ngxs/store'; 3 | import { StateReset } from 'ngxs-reset-plugin'; 4 | 5 | import { addressFormValueToHTML } from 'src/app/shared/address-form/address-form-value.interface'; 6 | import { FormsStateModel } from '../../models/froms-state-model.interface'; 7 | import { ShippingMethod } from '../../models/shipping-method.interface'; 8 | import { shippingMethods } from '../../models/shipping-methods.const'; 9 | import { FormsState } from '../../store/forms.state'; 10 | 11 | @Component({ 12 | selector: 'afn-review-page', 13 | templateUrl: './review-page.component.html', 14 | styleUrls: ['./review-page.component.css'] 15 | }) 16 | export class ReviewPageComponent implements OnInit { 17 | formsValues: FormsStateModel; 18 | addressFormValueToHTML = addressFormValueToHTML; 19 | shippingMethod: ShippingMethod; 20 | 21 | constructor(private store: Store) {} 22 | 23 | ngOnInit() { 24 | this.formsValues = this.store.selectSnapshot(FormsState); 25 | this.shippingMethod = shippingMethods.find((method) => method.id === this.formsValues?.shippingForm?.model?.method); 26 | this.store.dispatch(new StateReset(FormsState)); 27 | } 28 | 29 | maskCardNumber(cardNumber: number): string { 30 | const cardNumberString = cardNumber.toString().split(''); 31 | cardNumberString.splice(0, 12, '****-****-****-'); 32 | return cardNumberString.join(''); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/forms/components/shippping-page/shipping-page.component.css: -------------------------------------------------------------------------------- 1 | #shipping-method mat-radio-button { 2 | display: block; 3 | } 4 | 5 | #gift-options mat-form-field { 6 | display: block; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/forms/components/shippping-page/shipping-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 7 |
8 | 9 | 10 | 11 | {{ 'SHIPPING.SHIPPING_METHODS' | translate }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ shippingMethod.price ? (shippingMethod.price | currency) : '' }} 19 | {{ shippingMethod.type }}: 20 | 21 | {{ 'SHIPPING.GET_IT_BY' | translate }} 22 | {{ shippingMethod.arrival.min | date: 'E, MMM d' }} 23 | {{ shippingMethod.arrival.min ? '-' : '' }} 24 | {{ shippingMethod.arrival.max | date: 'E, MMM d' }} 25 | 26 | 27 | 28 | 29 | {{ 'ERRORS.REQUIRED' | translate }} 30 | 31 | 32 | 33 | 34 | {{ 'SHIPPING.IS_A_GIFT' | translate }} 35 | 36 | 37 | 38 | {{ 'SHIPPING.SHIPPING_METHODS' | translate }} 39 | 40 | 41 | {{ 'SHIPPING.INCLUDE_GIFT_RECEIPT' | translate }} 42 | 43 | 44 | {{ 'SHIPPING.RECIPIENT_NAME' | translate }} 45 | 46 | 47 | 48 | 49 | {{ 'SHIPPING.CUSTOM_MESSAGE' | translate }} 50 | 51 | 52 | 53 | {{ 'SHIPPING.GIFT_WRAP' | translate }} 54 | 55 | 56 | 57 |
58 | 62 | 63 | 67 |
68 |
69 | -------------------------------------------------------------------------------- /src/app/forms/components/shippping-page/shipping-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { RoutePath } from 'src/app/models/route-path.enum'; 6 | import { shippingMethods } from '../../models/shipping-methods.const'; 7 | 8 | @Component({ 9 | selector: 'afn-shipping-page', 10 | templateUrl: './shipping-page.component.html', 11 | styleUrls: ['./shipping-page.component.css'] 12 | }) 13 | export class ShippingPageComponent { 14 | shippingPageForm = this.fb.group({ 15 | method: [1, Validators.required], 16 | isAGift: [null], 17 | giftOptions: this.fb.group({ 18 | includeGiftRecipt: [false], 19 | name: [null], 20 | message: [null], 21 | wrap: [false] 22 | }) 23 | }); 24 | 25 | shippingMethods = shippingMethods; 26 | submitted = false; 27 | 28 | constructor(private fb: FormBuilder, private router: Router) {} 29 | 30 | onSubmit() { 31 | this.submitted = true; 32 | if (this.shippingPageForm.valid) { 33 | this.router.navigate([RoutePath.payment]); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/forms/forms-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { RoutePath } from '../models/route-path.enum'; 5 | import { DeliveryPageComponent } from './components/delivery-page/delivery-page.component'; 6 | import { FormsHomeComponent } from './components/forms-home/forms-home.component'; 7 | import { PaymentPageComponent } from './components/payment-page/payment-page.component'; 8 | import { ReviewPageComponent } from './components/review-page/review-page.component'; 9 | import { ShippingPageComponent } from './components/shippping-page/shipping-page.component'; 10 | import { CanActivateForms } from './services/can-activate-forms'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | component: FormsHomeComponent, 16 | children: [ 17 | { path: '', redirectTo: RoutePath.delivery }, 18 | { path: RoutePath.delivery, component: DeliveryPageComponent }, 19 | { path: RoutePath.shipping, component: ShippingPageComponent, canActivate: [CanActivateForms] }, 20 | { path: RoutePath.payment, component: PaymentPageComponent, canActivate: [CanActivateForms] } 21 | ] 22 | }, 23 | { 24 | path: RoutePath.review, 25 | component: ReviewPageComponent 26 | } 27 | ]; 28 | 29 | @NgModule({ 30 | imports: [RouterModule.forChild(routes)], 31 | exports: [RouterModule] 32 | }) 33 | export class FormsRoutingModule {} 34 | -------------------------------------------------------------------------------- /src/app/forms/forms.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgxsFormPluginModule } from '@ngxs/form-plugin'; 3 | import { NgxsModule } from '@ngxs/store'; 4 | import { NgxMaskModule } from 'ngx-mask'; 5 | import { SharedModule } from '../shared/shared.module'; 6 | import { DeliveryPageComponent } from './components/delivery-page/delivery-page.component'; 7 | import { FormsHomeComponent } from './components/forms-home/forms-home.component'; 8 | import { PaymentPageComponent } from './components/payment-page/payment-page.component'; 9 | import { ReviewPageComponent } from './components/review-page/review-page.component'; 10 | import { ShippingPageComponent } from './components/shippping-page/shipping-page.component'; 11 | import { FormsRoutingModule } from './forms-routing.module'; 12 | import { FormsState } from './store/forms.state'; 13 | 14 | @NgModule({ 15 | declarations: [DeliveryPageComponent, FormsHomeComponent, ShippingPageComponent, PaymentPageComponent, ReviewPageComponent], 16 | imports: [FormsRoutingModule, SharedModule, NgxMaskModule.forChild(), NgxsModule.forFeature([FormsState]), NgxsFormPluginModule] 17 | }) 18 | export class FormsModule {} 19 | -------------------------------------------------------------------------------- /src/app/forms/models/delivery-page-form-value.interface.ts: -------------------------------------------------------------------------------- 1 | import { AddressFormValue } from 'src/app/shared/address-form/address-form-value.interface'; 2 | 3 | export interface DeliveryPageFormValue { 4 | billingAddress: AddressFormValue; 5 | isShippingSame: boolean; 6 | shippingAddress?: AddressFormValue; 7 | createAccount: boolean; 8 | account: { 9 | email: string; 10 | password?: string; 11 | confirmPassword?: string; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/forms/models/delivery-page-form-value.mock.ts: -------------------------------------------------------------------------------- 1 | import { DeliveryPageFormValue } from './delivery-page-form-value.interface'; 2 | 3 | export const mockDeliveryPageFormValue: DeliveryPageFormValue = { 4 | billingAddress: { 5 | firstName: 'Joe', 6 | lastName: 'Doe', 7 | addressLine1: 'address line 1', 8 | city: 'city', 9 | province: 'province', 10 | country: 'country', 11 | postalCode: '12345' 12 | }, 13 | isShippingSame: true, 14 | createAccount: true, 15 | account: { 16 | email: 'joe@gmail.com', 17 | password: 'Password123.', 18 | confirmPassword: 'Password123.' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/forms/models/forms-state-model-init.const.ts: -------------------------------------------------------------------------------- 1 | import { initFormState } from 'src/app/models/forms-state-init.const'; 2 | import { FormsStateModel } from './froms-state-model.interface'; 3 | 4 | export const initFormsStateModel: FormsStateModel = { 5 | deliveryForm: initFormState, 6 | shippingForm: initFormState, 7 | paymentForm: initFormState 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/forms/models/froms-state-model.interface.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from 'src/app/models/forms-state.interface'; 2 | import { DeliveryPageFormValue } from './delivery-page-form-value.interface'; 3 | import { PaymentPageFormValue } from './payment-page-form-value.interface'; 4 | import { ShippingPageFormValue } from './shipping-page-form-value.interface'; 5 | 6 | export interface FormsStateModel { 7 | deliveryForm: FormState; 8 | shippingForm: FormState; 9 | paymentForm: FormState; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/forms/models/payment-page-form-value.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaymentPageFormValue { 2 | cardNumber: number; 3 | expiryDate: Date; 4 | cvv: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/forms/models/shipping-method.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ShippingMethod { 2 | id: number; 3 | type: string; 4 | price: number; 5 | arrival: { 6 | min?: Date; 7 | max: Date; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/forms/models/shipping-methods.const.ts: -------------------------------------------------------------------------------- 1 | import { add } from 'date-fns'; 2 | import { ShippingMethod } from './shipping-method.interface'; 3 | 4 | export const shippingMethods: ShippingMethod[] = [ 5 | { 6 | id: 1, 7 | type: 'Standard Shipping', 8 | price: 5.99, 9 | arrival: { 10 | max: add(new Date(), { days: 4 }) 11 | } 12 | }, 13 | { 14 | id: 2, 15 | type: 'FREE Shipping', 16 | price: 0, 17 | arrival: { 18 | min: add(new Date(), { days: 5 }), 19 | max: add(new Date(), { days: 7 }) 20 | } 21 | }, 22 | { 23 | id: 3, 24 | type: 'Two-Day Shipping', 25 | price: 8.99, 26 | arrival: { 27 | max: add(new Date(), { days: 2 }) 28 | } 29 | } 30 | ].sort((method1, method2) => method1.price - method2.price); 31 | -------------------------------------------------------------------------------- /src/app/forms/models/shipping-page-form-value.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ShippingPageFormValue { 2 | method: number; 3 | isAGift: boolean; 4 | giftOptions: { 5 | includeGiftRecipt: boolean; 6 | name: string; 7 | message: string; 8 | wrap: boolean; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/forms/services/can-activate-forms.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; 3 | import { Store } from '@ngxs/store'; 4 | import { Observable } from 'rxjs'; 5 | import { RoutePath } from 'src/app/models/route-path.enum'; 6 | import * as FormsSelectors from '../store/forms.selectors'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class CanActivateForms implements CanActivate { 12 | constructor(private store: Store) {} 13 | 14 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean { 15 | if (route.url[0].path === RoutePath.payment) { 16 | return this.store.select(FormsSelectors.isPaymentEnabled); 17 | } 18 | if (route.url[0].path === RoutePath.shipping) { 19 | return this.store.select(FormsSelectors.isDeliveryFormValid); 20 | } 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/forms/services/delivery-page-form.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockDeliveryPageFormValue } from '../models/delivery-page-form-value.mock'; 2 | import { deliveryPageFromValueValidator } from './delivery-page-form.validator'; 3 | 4 | describe('Delivery Page Form Validator', () => { 5 | it('should return null if form is valid', () => { 6 | const actual = deliveryPageFromValueValidator(mockDeliveryPageFormValue); 7 | const expected = null; 8 | expect(actual).toEqual(expected); 9 | }); 10 | 11 | it('should return shippingAddress if shipping address is not same and shipping address is missing', () => { 12 | const invalidForm = { 13 | billingAddress: { 14 | firstName: 'Joe', 15 | lastName: 'Doe', 16 | addressLine1: 'address line 1', 17 | city: 'city', 18 | province: 'province', 19 | country: 'country', 20 | postalCode: '12345' 21 | }, 22 | isShippingSame: false, 23 | createAccount: true, 24 | account: { 25 | email: 'joe@gmail.com', 26 | password: 'Password123.', 27 | confirmPassword: 'Password123.' 28 | } 29 | }; 30 | const actual = deliveryPageFromValueValidator(invalidForm); 31 | const expected = { shippingAddress: true }; 32 | expect(actual).toEqual(expected); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/app/forms/services/delivery-page-form.validator.ts: -------------------------------------------------------------------------------- 1 | import { FormGroup } from '@angular/forms'; 2 | 3 | import { DeliveryPageFormValue } from '../models/delivery-page-form-value.interface'; 4 | 5 | export function deliveryPageFromValidator(deliveryPageFrom: FormGroup): { [key: string]: any } | null { 6 | const formValue: DeliveryPageFormValue = deliveryPageFrom.value; 7 | return deliveryPageFromValueValidator(formValue); 8 | } 9 | 10 | export function deliveryPageFromValueValidator(deliveryPageFromValue: DeliveryPageFormValue): { [key: string]: any } | null { 11 | const errors = {}; 12 | if (!deliveryPageFromValue.isShippingSame && !deliveryPageFromValue.shippingAddress) { 13 | errors['shippingAddress'] = true; 14 | } 15 | if (deliveryPageFromValue.createAccount) { 16 | if (!deliveryPageFromValue.account.password) { 17 | errors['password'] = true; 18 | } 19 | if (!deliveryPageFromValue.account.confirmPassword) { 20 | errors['confirmPassword'] = true; 21 | } else if (deliveryPageFromValue.account.password !== deliveryPageFromValue.account.confirmPassword) { 22 | errors['passwordMatch'] = true; 23 | } 24 | } 25 | return Object.keys(errors).length ? errors : null; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/forms/store/forms.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from '@ngxs/store'; 2 | import { AppStateModel } from 'src/app/models/app-state-model.interface'; 3 | 4 | export const formState = (appState: AppStateModel) => appState.forms; 5 | 6 | export const deliveryForm = (appState: AppStateModel) => formState(appState).deliveryForm; 7 | 8 | export const shippingForm = (appState: AppStateModel) => formState(appState).shippingForm; 9 | 10 | export const paymentForm = (appState: AppStateModel) => formState(appState).paymentForm; 11 | 12 | export const isDeliveryFormValid = (appState: AppStateModel) => deliveryForm(appState).status === 'VALID'; 13 | 14 | export const isShippingFormValid = (appState: AppStateModel) => shippingForm(appState).status === 'VALID'; 15 | 16 | export const isPaymentFormValid = (appState: AppStateModel) => paymentForm(appState).status === 'VALID'; 17 | 18 | export const isPaymentEnabled = createSelector( 19 | [isDeliveryFormValid, isShippingFormValid], 20 | (deliveryFormValid, shippingFormValid) => deliveryFormValid && shippingFormValid 21 | ); 22 | -------------------------------------------------------------------------------- /src/app/forms/store/forms.state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { State } from '@ngxs/store'; 3 | 4 | import { initFormsStateModel } from '../models/forms-state-model-init.const'; 5 | 6 | @State({ 7 | name: 'forms', 8 | defaults: initFormsStateModel 9 | }) 10 | @Injectable() 11 | export class FormsState {} 12 | -------------------------------------------------------------------------------- /src/app/models/app-state-model.interface.ts: -------------------------------------------------------------------------------- 1 | import { FormsStateModel } from '../forms/models/froms-state-model.interface'; 2 | 3 | export interface AppStateModel { 4 | forms: FormsStateModel; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/forms-state-init.const.ts: -------------------------------------------------------------------------------- 1 | import { FormState } from './forms-state.interface'; 2 | 3 | export const initFormState: FormState = { 4 | model: undefined, 5 | dirty: false, 6 | status: '', 7 | errors: null 8 | }; 9 | -------------------------------------------------------------------------------- /src/app/models/forms-state.interface.ts: -------------------------------------------------------------------------------- 1 | import { ValidationErrors } from '@angular/forms'; 2 | 3 | export interface FormState { 4 | model: T; 5 | dirty: boolean; 6 | status: string; 7 | errors: ValidationErrors | null; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/models/route-path.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RoutePath { 2 | delivery = 'delivery', 3 | shipping = 'shipping', 4 | payment = 'payment', 5 | review = 'review' 6 | } 7 | -------------------------------------------------------------------------------- /src/app/shared/address-form/address-form-value.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AddressFormValue { 2 | firstName: string; 3 | lastName: string; 4 | addressLine1: string; 5 | addressLine2?: string; 6 | city: string; 7 | province: string; 8 | country: string; 9 | postalCode: string; 10 | } 11 | 12 | export function addressFormValueToHTML(addressFormValue: AddressFormValue): string { 13 | if (!addressFormValue) { 14 | return ''; 15 | } 16 | return ( 17 | addressFormValue.firstName + 18 | ' ' + 19 | addressFormValue.lastName + 20 | '
' + 21 | addressFormValue.addressLine1 + 22 | ' ' + 23 | addressFormValue.addressLine2 + 24 | '
' + 25 | addressFormValue.city + 26 | ', ' + 27 | addressFormValue.province + 28 | ', ' + 29 | addressFormValue.country + 30 | '
' + 31 | addressFormValue.postalCode 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/address-form/address-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | {{ 'ADDRESS.FIRST_NAME' | translate }} 5 | 6 | 7 | {{ 'ERRORS.REQUIRED' | translate }} 8 | 9 | 10 | {{ 'ERRORS.PATTERN' | translate }} 11 | 12 | 13 | 14 | 15 | {{ 'ADDRESS.LAST_NAME' | translate }} 16 | 17 | 18 | {{ 'ERRORS.REQUIRED' | translate }} 19 | 20 | 21 | {{ 'ERRORS.PATTERN' | translate }} 22 | 23 | 24 |
25 | 26 | 27 | {{ 'ADDRESS.ADDRESS_LINE_1' | translate }} 28 | 29 | 30 | {{ 'ERRORS.REQUIRED' | translate }} 31 | 32 | 33 | 34 | 35 | {{ 'ADDRESS.ADDRESS_LINE_2' | translate }} 36 | 37 | 38 | 39 |
40 | 41 | {{ 'ADDRESS.CITY' | translate }} 42 | 43 | 44 | {{ 'ERRORS.REQUIRED' | translate }} 45 | 46 | 47 | 48 | 49 | {{ 'ADDRESS.PROVINCE' | translate }} 50 | 51 | 52 | {{ 'ERRORS.REQUIRED' | translate }} 53 | 54 | 55 | 56 | 57 | {{ 'ADDRESS.COUNTRY' | translate }} 58 | 59 | 60 | {{ 'ERRORS.REQUIRED' | translate }} 61 | 62 | 63 | 64 | 65 | {{ 'ADDRESS.POSTAL_CODE' | translate }} 66 | 67 | 68 | {{ 'ERRORS.REQUIRED' | translate }} 69 | 70 | 71 |
72 |
73 | -------------------------------------------------------------------------------- /src/app/shared/address-form/address-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, forwardRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; 2 | import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { nameRegex } from '../regexes.const'; 6 | import { AddressFormValue } from './address-form-value.interface'; 7 | 8 | @Component({ 9 | selector: 'afn-address-form', 10 | templateUrl: './address-form.component.html', 11 | providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AddressFormComponent), multi: true }] 12 | }) 13 | export class AddressFormComponent implements ControlValueAccessor, OnInit, OnDestroy, OnChanges { 14 | @Input() touched: boolean; 15 | 16 | addressForm = this.fb.group({ 17 | firstName: [null, [Validators.required, Validators.pattern(nameRegex)]], 18 | lastName: [null, [Validators.required, Validators.pattern(nameRegex)]], 19 | addressLine1: [null, Validators.required], 20 | addressLine2: [null], 21 | city: [null, Validators.required], 22 | province: [null, Validators.required], 23 | country: [null, Validators.required], 24 | postalCode: [null, Validators.required] 25 | }); 26 | 27 | private subscription = new Subscription(); 28 | 29 | onChange: any = (_: AddressFormValue) => {}; 30 | onTouch: any = () => {}; 31 | 32 | constructor(private fb: FormBuilder) {} 33 | 34 | ngOnInit() { 35 | this.subscription.add( 36 | this.addressForm.valueChanges.subscribe((value: AddressFormValue) => { 37 | this.onChange(value); 38 | }) 39 | ); 40 | } 41 | 42 | ngOnDestroy() { 43 | this.subscription.unsubscribe(); 44 | } 45 | 46 | ngOnChanges(simpleChanges: SimpleChanges) { 47 | if (simpleChanges['touched'] && simpleChanges['touched'].currentValue) { 48 | this.addressForm.markAllAsTouched(); 49 | } 50 | } 51 | 52 | writeValue(value: null | AddressFormValue): void { 53 | if (value) { 54 | this.addressForm.reset(value); 55 | } 56 | } 57 | 58 | registerOnChange(fn: () => {}): void { 59 | this.onChange = fn; 60 | } 61 | 62 | registerOnTouched(fn: (_: AddressFormValue) => {}): void { 63 | this.onTouch = fn; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/app/shared/form-group-error-state-matcher.ts: -------------------------------------------------------------------------------- 1 | import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; 2 | import { ErrorStateMatcher } from '@angular/material/core'; 3 | 4 | export class FormGroupErrorStateMatcher implements ErrorStateMatcher { 5 | errors: string[]; 6 | 7 | constructor(errors: string[]) { 8 | this.errors = errors; 9 | } 10 | 11 | isErrorState(control: FormControl | null, formDirective: FormGroupDirective | NgForm | null): boolean { 12 | const formGroupHasError = this.errors.reduce((total, error) => total || formDirective.form.hasError(error), false); 13 | return control.touched && (control.invalid || formGroupHasError); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/shared/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatCardModule } from '@angular/material/card'; 4 | import { MatCheckboxModule } from '@angular/material/checkbox'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatRadioModule } from '@angular/material/radio'; 10 | import { MatTabsModule } from '@angular/material/tabs'; 11 | 12 | const modules = [ 13 | MatButtonModule, 14 | MatCardModule, 15 | MatTabsModule, 16 | MatFormFieldModule, 17 | MatIconModule, 18 | MatInputModule, 19 | MatCheckboxModule, 20 | MatRadioModule, 21 | MatListModule 22 | ]; 23 | 24 | @NgModule({ 25 | imports: modules, 26 | exports: modules 27 | }) 28 | export class MaterialModule {} 29 | -------------------------------------------------------------------------------- /src/app/shared/regexes.const.ts: -------------------------------------------------------------------------------- 1 | // regex taken from https://stackoverflow.com/questions/2385701/regular-expression-for-first-and-last-name 2 | export const nameRegex = /^[a-z ,.'-]+$/i; 3 | 4 | // regex taken from 5 | // https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a 6 | export const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; 7 | 8 | export const numberOnlyRegex = /^[0-9]*$/; 9 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | 6 | import { AddressFormComponent } from './address-form/address-form.component'; 7 | import { MaterialModule } from './material.module'; 8 | 9 | @NgModule({ 10 | imports: [MaterialModule, TranslateModule, CommonModule, FormsModule, ReactiveFormsModule], 11 | declarations: [AddressFormComponent], 12 | exports: [AddressFormComponent, MaterialModule, TranslateModule, CommonModule, FormsModule, ReactiveFormsModule] 13 | }) 14 | export class SharedModule {} 15 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiongemi/angular-form-ngxs/56c4018052e70aeab744b3e365d2dfbe0e898a4e/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "TABS": { 3 | "DELIVERY": "Delivery", 4 | "SHIPPING": "Shipping", 5 | "PAYMENT": "Payment" 6 | }, 7 | "DELIVERY": { 8 | "SHIPPING_SAME": "My shipping address is same as my billing address.", 9 | "USER_CHECKOUT_OPTIONS": "User checkout options", 10 | "CREATE_ACCOUNT": "Create Account", 11 | "CHECKOUT_AS_GUEST": "Checkout as a Guest", 12 | "EMAIL": "Email", 13 | "PASSWORD": "Password", 14 | "CONFIRM_PASSWORD": "Confirm Password", 15 | "PASSWORD_MATCH": "Passwords must match", 16 | "PASSWORD_HINT": "Minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character (@$!%*#?&)" 17 | }, 18 | "SHIPPING": { 19 | "SHIPPING_METHODS": "Shipping Methods", 20 | "GET_IT_BY": "get it by", 21 | "IS_A_GIFT": "Item is a gift", 22 | "GIFT_OPTIONS": "Gift Options", 23 | "INCLUDE_GIFT_RECEIPT": "Include gift receipt", 24 | "RECIPIENT_NAME": "Recipient Name", 25 | "CUSTOM_MESSAGE": "Custom Message", 26 | "GIFT_WRAP": "Add gift wrap" 27 | }, 28 | "PAYMENT": { 29 | "PAYMENT_METHOD": "Payment Method", 30 | "CARD_NUMBER": "Card Number", 31 | "EXPIRY_DATE": "Expiry Date", 32 | "CVV": "CVV" 33 | }, 34 | "ADDRESS": { 35 | "FIRST_NAME": "First Name", 36 | "LAST_NAME": "Last Name", 37 | "ADDRESS_LINE_1": "Address Line 1", 38 | "ADDRESS_LINE_2": "Address Line 2", 39 | "CITY": "City / District", 40 | "PROVINCE": "Province / State", 41 | "COUNTRY": "Country", 42 | "POSTAL_CODE": "Postal Code", 43 | "SHIPPING_ADDRESS": "Shipping Address", 44 | "BILLING_ADDRESS": "Billing Address" 45 | }, 46 | "ERRORS": { 47 | "REQUIRED": "Field is required", 48 | "PATTERN": "Invalid format", 49 | "FORM_ERROR": "Please fix all the errors." 50 | }, 51 | "COMMON": { 52 | "NEXT": "Next", 53 | "PAY": "Pay", 54 | "PREVIOUS": "Previous", 55 | "RESET": "Reset", 56 | "REVIEW": "Review", 57 | "EDIT": "Edit", 58 | "RESTART": "Restart" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiongemi/angular-form-ngxs/56c4018052e70aeab744b3e365d2dfbe0e898a4e/src/assets/icon.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiongemi/angular-form-ngxs/56c4018052e70aeab744b3e365d2dfbe0e898a4e/src/icon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Checkout Form with NGXS 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /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.ts'; 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 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @import '~tachyons/css/tachyons.min'; 4 | @import '@angular/material/prebuilt-themes/indigo-pink.css'; 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | font-family: Roboto, 'Helvetica Neue', sans-serif; 14 | } 15 | 16 | .mat-tab-links { 17 | justify-content: center; 18 | } 19 | 20 | .mat-checkbox-layout { 21 | white-space: normal !important; 22 | } 23 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | import 'zone.js/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting(), { 16 | teardown: { destroyAfterEach: false } 17 | } 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /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 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "es2020", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------