├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── ngsw-config.json ├── package-lock.json ├── package.json ├── screenshots ├── completed-todos.PNG └── todos.PNG ├── server ├── assets │ └── i18n │ │ ├── da-lang.json │ │ └── en-lang.json └── index.js ├── src ├── app │ ├── app-init.service.spec.ts │ ├── app-init.service.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── core │ │ ├── core.module.ts │ │ ├── spinner-overlay │ │ │ ├── spinner-overlay.component.html │ │ │ ├── spinner-overlay.component.scss │ │ │ ├── spinner-overlay.component.spec.ts │ │ │ ├── spinner-overlay.component.ts │ │ │ ├── spinner-overlay.module.ts │ │ │ ├── spinner-overlay.service.spec.ts │ │ │ └── spinner-overlay.service.ts │ │ ├── store │ │ │ ├── generic-action.ts │ │ │ └── store.module.ts │ │ └── todo-list │ │ │ ├── redux-api │ │ │ ├── todo-list-store.module.ts │ │ │ ├── todo-list.actions.spec.ts │ │ │ ├── todo-list.actions.ts │ │ │ ├── todo-list.effects.spec.ts │ │ │ ├── todo-list.effects.ts │ │ │ ├── todo-list.model.ts │ │ │ ├── todo-list.reducers.spec.ts │ │ │ ├── todo-list.reducers.ts │ │ │ ├── todo-list.selector.spec.ts │ │ │ └── todo-list.selector.ts │ │ │ ├── todo-list.service.spec.ts │ │ │ └── todo-list.service.ts │ ├── footer │ │ ├── footer.component.css │ │ ├── footer.component.html │ │ ├── footer.component.mock.ts │ │ ├── footer.component.spec.ts │ │ └── footer.component.ts │ ├── helpers │ │ └── spy-helper.ts │ ├── navbar │ │ ├── navbar.component.html │ │ ├── navbar.component.mock.ts │ │ ├── navbar.component.scss │ │ ├── navbar.component.spec.ts │ │ └── navbar.component.ts │ ├── shared │ │ ├── app-material │ │ │ └── app-material.module.ts │ │ ├── cards-list │ │ │ ├── cards-list.component.html │ │ │ ├── cards-list.component.scss │ │ │ ├── cards-list.component.ts │ │ │ ├── cards-list.module.ts │ │ │ ├── cards │ │ │ │ ├── cards.component.html │ │ │ │ ├── cards.component.scss │ │ │ │ ├── cards.component.spec.ts │ │ │ │ └── cards.component.ts │ │ │ └── list │ │ │ │ ├── list.component.html │ │ │ │ ├── list.component.scss │ │ │ │ ├── list.component.spec.ts │ │ │ │ └── list.component.ts │ │ ├── invalid-date.directive.ts │ │ ├── models │ │ │ ├── guid.ts │ │ │ └── todo-item.ts │ │ ├── shared.module.ts │ │ ├── spinner-overlay-wrapper │ │ │ ├── spinner-overlay-wrapper.component.html │ │ │ ├── spinner-overlay-wrapper.component.scss │ │ │ ├── spinner-overlay-wrapper.component.spec.ts │ │ │ ├── spinner-overlay-wrapper.component.ts │ │ │ └── spinner-overlay-wrapper.module.ts │ │ ├── todo-item-card │ │ │ ├── todo-item-card.component.html │ │ │ ├── todo-item-card.component.mock.ts │ │ │ ├── todo-item-card.component.scss │ │ │ ├── todo-item-card.component.spec.ts │ │ │ └── todo-item-card.component.ts │ │ └── todo-item-list-row │ │ │ ├── todo-item-list-row.component.html │ │ │ ├── todo-item-list-row.component.mock.ts │ │ │ ├── todo-item-list-row.component.scss │ │ │ ├── todo-item-list-row.component.spec.ts │ │ │ └── todo-item-list-row.component.ts │ ├── todo-list-completed │ │ ├── todo-list-completed.component.html │ │ ├── todo-list-completed.component.scss │ │ ├── todo-list-completed.component.spec.ts │ │ ├── todo-list-completed.component.ts │ │ ├── todo-list-completed.module.ts │ │ └── todo-list-completed.routing.ts │ └── todo-list │ │ ├── add-todo │ │ ├── add-todo-presentation │ │ │ ├── add-todo-presentation.component.html │ │ │ ├── add-todo-presentation.component.scss │ │ │ ├── add-todo-presentation.component.spec.ts │ │ │ └── add-todo-presentation.component.ts │ │ ├── add-todo.component.css │ │ ├── add-todo.component.html │ │ ├── add-todo.component.mock.ts │ │ ├── add-todo.component.spec.ts │ │ ├── add-todo.component.ts │ │ ├── add-todo.module.ts │ │ ├── add-todo.service.spec.ts │ │ └── add-todo.service.ts │ │ ├── duedate-today-count.pipe.spec.ts │ │ ├── duedate-today-count.pipe.ts │ │ ├── todo-list.component.html │ │ ├── todo-list.component.scss │ │ ├── todo-list.component.spec.ts │ │ ├── todo-list.component.ts │ │ └── todo-list.module.ts ├── assets │ ├── .gitkeep │ ├── app-config.json │ └── icons │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png ├── environments │ ├── dynamic-environment.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── manifest.json ├── polyfills.ts ├── shared-lib │ └── spinner │ │ ├── spinner.component.html │ │ ├── spinner.component.mock.ts │ │ ├── spinner.component.scss │ │ ├── spinner.component.spec.ts │ │ ├── spinner.component.ts │ │ └── spinner.module.ts ├── styles.scss ├── styles │ ├── positioning.scss │ └── spinner.scss ├── test.ts └── tsconfig.app.json ├── tsconfig.app.json ├── tsconfig.default.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "tslint.autoFixOnSave": true 4 | }, 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AngularDemoWithBestPracticesV8 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.3. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | 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`. 12 | 13 | ## Build 14 | 15 | 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. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | 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). 28 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "Angular-demo-with-best-practices": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss", 11 | "changeDetection": "OnPush" 12 | } 13 | }, 14 | "root": "", 15 | "sourceRoot": "src", 16 | "prefix": "app", 17 | "architect": { 18 | "build": { 19 | "builder": "@angular-devkit/build-angular:browser", 20 | "options": { 21 | "outputPath": "dist/Angular-demo-with-best-practices", 22 | "index": "src/index.html", 23 | "main": "src/main.ts", 24 | "polyfills": "src/polyfills.ts", 25 | "tsConfig": "tsconfig.app.json", 26 | "aot": false, 27 | "assets": [ 28 | "src/favicon.ico", 29 | "src/assets", 30 | "src/manifest.json" 31 | ], 32 | "styles": [ 33 | "src/styles.scss" 34 | ], 35 | "scripts": [] 36 | }, 37 | "configurations": { 38 | "production": { 39 | "fileReplacements": [ 40 | { 41 | "replace": "src/environments/environment.ts", 42 | "with": "src/environments/environment.prod.ts" 43 | } 44 | ], 45 | "optimization": true, 46 | "outputHashing": "all", 47 | "sourceMap": false, 48 | "extractCss": true, 49 | "namedChunks": false, 50 | "aot": true, 51 | "extractLicenses": true, 52 | "vendorChunk": false, 53 | "buildOptimizer": true, 54 | "budgets": [ 55 | { 56 | "type": "initial", 57 | "maximumWarning": "2mb", 58 | "maximumError": "5mb" 59 | } 60 | ] 61 | } 62 | } 63 | }, 64 | "serve": { 65 | "builder": "@angular-devkit/build-angular:dev-server", 66 | "options": { 67 | "browserTarget": "Angular-demo-with-best-practices:build" 68 | }, 69 | "configurations": { 70 | "production": { 71 | "browserTarget": "Angular-demo-with-best-practices:build:production" 72 | } 73 | } 74 | }, 75 | "extract-i18n": { 76 | "builder": "@angular-devkit/build-angular:extract-i18n", 77 | "options": { 78 | "browserTarget": "Angular-demo-with-best-practices:build" 79 | } 80 | }, 81 | "test": { 82 | "builder": "@angular-devkit/build-angular:karma", 83 | "options": { 84 | "main": "src/test.ts", 85 | "polyfills": "src/polyfills.ts", 86 | "tsConfig": "tsconfig.spec.json", 87 | "karmaConfig": "karma.conf.js", 88 | "assets": [ 89 | "src/favicon.ico", 90 | "src/assets", 91 | "src/manifest.json" 92 | ], 93 | "styles": [ 94 | "src/styles.scss" 95 | ], 96 | "scripts": [] 97 | } 98 | }, 99 | "lint": { 100 | "builder": "@angular-devkit/build-angular:tslint", 101 | "options": { 102 | "tsConfig": [ 103 | "tsconfig.app.json", 104 | "tsconfig.spec.json", 105 | "e2e/tsconfig.json" 106 | ], 107 | "exclude": [ 108 | "**/node_modules/**" 109 | ] 110 | } 111 | }, 112 | "e2e": { 113 | "builder": "@angular-devkit/build-angular:protractor", 114 | "options": { 115 | "protractorConfig": "e2e/protractor.conf.js", 116 | "devServerTarget": "Angular-demo-with-best-practices:serve" 117 | }, 118 | "configurations": { 119 | "production": { 120 | "devServerTarget": "Angular-demo-with-best-practices:serve:production" 121 | } 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "defaultProject": "Angular-demo-with-best-practices" 128 | } 129 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { 6 | SpecReporter 7 | } = require('jasmine-spec-reporter'); 8 | 9 | /** 10 | * @type { import("protractor").Config } 11 | */ 12 | exports.config = { 13 | allScriptsTimeout: 11000, 14 | specs: [ 15 | './src/**/*.e2e-spec.ts' 16 | ], 17 | capabilities: { 18 | 'browserName': 'chrome' 19 | }, 20 | directConnect: true, 21 | baseUrl: 'http://localhost:4200/', 22 | framework: 'jasmine', 23 | jasmineNodeOpts: { 24 | showColors: true, 25 | defaultTimeoutInterval: 30000, 26 | print: function () {} 27 | }, 28 | onPrepare() { 29 | require('ts-node').register({ 30 | project: require('path').join(__dirname, './tsconfig.json') 31 | }); 32 | jasmine.getEnv().addReporter(new SpecReporter({ 33 | spec: { 34 | displayStacktrace: true 35 | } 36 | })); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | 3 | import { AppPage } from './app.po'; 4 | 5 | describe('workspace-project App', () => { 6 | let page: AppPage; 7 | 8 | beforeEach(() => { 9 | page = new AppPage(); 10 | }); 11 | 12 | it('should display welcome message', () => { 13 | page.navigateTo(); 14 | expect(page.getTitleText()).toEqual('Welcome to Angular-demo-with-best-practices-v8!'); 15 | }); 16 | 17 | afterEach(async () => { 18 | // Assert that there are no errors emitted from the browser 19 | const logs = await browser 20 | .manage() 21 | .logs() 22 | .get(logging.Type.BROWSER); 23 | expect(logs).not.toContain( 24 | jasmine.objectContaining({ 25 | level: logging.Level.SEVERE 26 | } as logging.Entry) 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | public navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | public getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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-demo-with-best-practices-v8'), 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 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "dataGroups": [{ 4 | "name": "api-performance", 5 | "urls": [ 6 | "/assets/i18n/**" 7 | ], 8 | "cacheConfig": { 9 | "strategy": "performance", 10 | "maxSize": 100, 11 | "maxAge": "3d" 12 | } 13 | }, 14 | { 15 | "name": "api-freshness", 16 | "urls": [ 17 | "/api/fresh-todo-list", 18 | "/api/**" 19 | ], 20 | "cacheConfig": { 21 | "strategy": "freshness", 22 | "maxSize": 100, 23 | "maxAge": "3d", 24 | "timeout": "10s" 25 | } 26 | } 27 | ], 28 | "assetGroups": [{ 29 | "name": "app", 30 | "installMode": "prefetch", 31 | "resources": { 32 | "files": [ 33 | "/favicon.ico", 34 | "/index.html", 35 | "/*.css", 36 | "/*.js" 37 | ] 38 | } 39 | }, { 40 | "name": "assets", 41 | "installMode": "lazy", 42 | "updateMode": "prefetch", 43 | "resources": { 44 | "files": [ 45 | "/assets/**" 46 | ] 47 | } 48 | }] 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-demo-with-best-practices", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:all": "nodemon server | ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "prettier": { 14 | "singleQuote": true, 15 | "printWidth": 100, 16 | "semi": true, 17 | "bracketSpacing": true, 18 | "arrowParens": "always" 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "@angular/animations": "^8.0.3", 23 | "@angular/cdk": "^8.0.2", 24 | "@angular/common": "~8.0.1", 25 | "@angular/compiler": "~8.0.1", 26 | "@angular/core": "~8.0.1", 27 | "@angular/forms": "~8.0.1", 28 | "@angular/material": "^8.0.2", 29 | "@angular/platform-browser": "~8.0.1", 30 | "@angular/platform-browser-dynamic": "~8.0.1", 31 | "@angular/router": "~8.0.1", 32 | "@angular/service-worker": "^8.1.0", 33 | "@ng-bootstrap/ng-bootstrap": "^5.0.0-rc.1", 34 | "@ngrx/effects": "^8.0.1", 35 | "@ngrx/store": "^8.0.1", 36 | "@ngrx/store-devtools": "^8.0.1", 37 | "@ngx-translate/core": "^11.0.1", 38 | "@ngx-translate/http-loader": "^4.0.0", 39 | "bootstrap": "^4.3.1", 40 | "cors": "^2.8.5", 41 | "hammerjs": "^2.0.8", 42 | "jasmine-marbles": "^0.6.0", 43 | "rxjs": "~6.4.0", 44 | "tslib": "^1.9.0", 45 | "zone.js": "~0.9.1" 46 | }, 47 | "devDependencies": { 48 | "@angular-devkit/build-angular": "~0.800.0", 49 | "@angular/cli": "~8.0.3", 50 | "@angular/compiler-cli": "~8.0.1", 51 | "@angular/language-service": "~8.0.1", 52 | "@types/jasmine": "^3.3.13", 53 | "@types/jasminewd2": "^2.0.6", 54 | "@types/node": "~8.9.4", 55 | "codelyzer": "^5.0.0", 56 | "jasmine-core": "~3.4.0", 57 | "jasmine-spec-reporter": "~4.2.1", 58 | "karma": "~4.1.0", 59 | "karma-chrome-launcher": "~2.2.0", 60 | "karma-coverage-istanbul-reporter": "~2.0.1", 61 | "karma-jasmine": "~2.0.1", 62 | "karma-jasmine-html-reporter": "^1.4.0", 63 | "nodemon": "^1.19.1", 64 | "protractor": "~5.4.0", 65 | "ts-node": "~7.0.0", 66 | "tslint": "~5.15.0", 67 | "tslint-config-prettier": "^1.18.0", 68 | "tslint-eslint-rules": "^5.4.0", 69 | "tslint-jasmine-rules": "^1.6.0", 70 | "typescript": "~3.4.3" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /screenshots/completed-todos.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/screenshots/completed-todos.PNG -------------------------------------------------------------------------------- /screenshots/todos.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/screenshots/todos.PNG -------------------------------------------------------------------------------- /server/assets/i18n/da-lang.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-title": "TODO app", 3 | "choose-language": "Vælg sprog", 4 | "todo-list": "TODO liste", 5 | "completed-todos": "Udførte TODOs", 6 | "errors": { 7 | "invalid-form": "Ugyldig form" 8 | }, 9 | "todo-item": { 10 | "completed": "Udført", 11 | "edit": "Redigér", 12 | "delete": "Slet" 13 | }, 14 | "todo-list-section": { 15 | "show-cards": "Vis kort", 16 | "show-list": "Vis liste", 17 | "todos-duedate-today": "Udløber i dag" 18 | }, 19 | "add-todo": { 20 | "headline": "Tilføj TODO", 21 | "title": "Titel", 22 | "description": "Beskrivelse", 23 | "due-date": "Forfaldsdato", 24 | "create": "Opret" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/assets/i18n/en-lang.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-title": "TODO app", 3 | "choose-language": "Choose language", 4 | "todo-list": "TODO list", 5 | "completed-todos": "Completed TODOs", 6 | "errors": { 7 | "invalid-form": "Invalid form" 8 | }, 9 | "todo-item": { 10 | "complete": "Complete", 11 | "edit": "Edit", 12 | "delete": "Delete" 13 | }, 14 | "todo-list-section": { 15 | "show-cards": "Show cards", 16 | "show-list": "Show list", 17 | "todos-duedate-today": "Expires today" 18 | }, 19 | "add-todo": { 20 | "headline": "Add TODO", 21 | "title": "Title", 22 | "description": "Description", 23 | "due-date": "Due date", 24 | "create": "Create" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | const express = require('express'); 3 | var cors = require('cors') 4 | var app = express(); 5 | 6 | app.use(cors()); 7 | 8 | app.use('/assets', express.static(path.resolve(__dirname, 'assets'))); 9 | var apiRoutes = express.Router(); 10 | app.use('/api', apiRoutes) 11 | apiRoutes.get('/todo-list', (req, res) => { 12 | 13 | const todoList = [{ 14 | id: 'task1', 15 | title: 'Buy Milk', 16 | description: 'Remember to buy milk' 17 | }, 18 | { 19 | id: 'task2', 20 | title: 'Go to the gym', 21 | description: 'Remember to work out' 22 | } 23 | ]; 24 | return res.json(todoList); 25 | }); 26 | 27 | const port = 8080; 28 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 29 | -------------------------------------------------------------------------------- /src/app/app-init.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { inject, TestBed } from '@angular/core/testing'; 4 | 5 | import { AppInitService } from './app-init.service'; 6 | 7 | describe('Service: AppInit', () => { 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [AppInitService] 11 | }); 12 | }); 13 | 14 | it('should ...', inject([AppInitService], (service: AppInitService) => { 15 | expect(service).toBeTruthy(); 16 | })); 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/app-init.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { from } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | declare var window: any; 5 | 6 | @Injectable() 7 | export class AppInitService { 8 | // This is the method you want to call at bootstrap 9 | // Important: It should return a Promise 10 | public init() { 11 | return from( 12 | fetch('assets/app-config.json').then((response) => { 13 | return response.json(); 14 | }) 15 | ) 16 | .pipe( 17 | map((config) => { 18 | window.config = config; 19 | return config; 20 | }) 21 | ) 22 | .toPromise(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule {} 11 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
-------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | 5 | import { AppComponent } from '@app/app.component'; 6 | import { TodoListService } from './core/todo-list/todo-list.service'; 7 | import { FooterComponentMock } from './footer/FooterComponentMock'; 8 | import { provideMagicalMock } from './helpers/spy-helper'; 9 | import { NavbarComponentMock } from './navbar/navbar.component.mock'; 10 | 11 | describe('AppComponent', () => { 12 | beforeEach(async(() => { 13 | TestBed.configureTestingModule({ 14 | declarations: [AppComponent, NavbarComponentMock, FooterComponentMock], 15 | imports: [], 16 | providers: [ 17 | { provide: APP_BASE_HREF, useValue: '/' }, 18 | provideMagicalMock(TranslateService), 19 | provideMagicalMock(TodoListService) 20 | ] 21 | }) 22 | .overrideTemplate(AppComponent, '') 23 | .compileComponents(); 24 | })); 25 | 26 | let translateServiceMock: jasmine.SpyObj; 27 | it('should create the app', async(() => { 28 | translateServiceMock = TestBed.get(TranslateService); 29 | translateServiceMock.getBrowserLang.and.returnValue('en'); 30 | const fixture = TestBed.createComponent(AppComponent); 31 | 32 | const app = fixture.debugElement.componentInstance; 33 | expect(app).toBeTruthy(); 34 | })); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | 4 | import { TodoListService } from './core/todo-list/todo-list.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class AppComponent implements OnInit { 13 | constructor(translate: TranslateService, private todoListService: TodoListService) { 14 | translate.addLangs(['en', 'da']); 15 | translate.setDefaultLang('en'); 16 | 17 | const browserLang = translate.getBrowserLang(); 18 | translate.use(browserLang.match(/en|da/) ? browserLang : 'en'); 19 | } 20 | 21 | public ngOnInit(): void { 22 | this.todoListService.loadTodoList(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpClientModule } from '@angular/common/http'; 2 | import { APP_INITIALIZER, NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 7 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 8 | import { environment } from 'environments/environment'; 9 | 10 | import { AppInitService } from '@app/app-init.service'; 11 | import { AppComponent } from '@app/app.component'; 12 | import { appRouterModule } from '@app/app.routes'; 13 | import { CoreModule } from '@app/core/core.module'; 14 | import { FooterComponent } from '@app/footer/footer.component'; 15 | import { NavbarComponent } from '@app/navbar/navbar.component'; 16 | import { SharedModule } from '@app/shared/shared.module'; 17 | import { TodoListModule } from '@app/todo-list/todo-list.module'; 18 | 19 | export function init_app(appLoadService: AppInitService) { 20 | return () => appLoadService.init(); 21 | } 22 | export function HttpLoaderFactory(httpClient: HttpClient) { 23 | return new TranslateHttpLoader( 24 | httpClient, 25 | environment.feServerUrl + '/assets/i18n/', 26 | '-lang.json' 27 | ); 28 | } 29 | @NgModule({ 30 | declarations: [AppComponent, NavbarComponent, FooterComponent], 31 | imports: [ 32 | BrowserModule, 33 | FormsModule, 34 | CoreModule, 35 | SharedModule, 36 | HttpClientModule, 37 | appRouterModule, 38 | TodoListModule, 39 | TranslateModule.forRoot({ 40 | loader: { 41 | provide: TranslateLoader, 42 | useFactory: HttpLoaderFactory, 43 | deps: [HttpClient] 44 | } 45 | }), 46 | ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }) 47 | ], 48 | providers: [ 49 | AppInitService, 50 | AppInitService, 51 | { 52 | provide: APP_INITIALIZER, 53 | useFactory: init_app, 54 | deps: [AppInitService], 55 | multi: true 56 | } 57 | ], 58 | bootstrap: [AppComponent] 59 | }) 60 | export class AppModule {} 61 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Routes } from '@angular/router'; 2 | 3 | import { TodoListComponent } from '@app/todo-list/todo-list.component'; 4 | 5 | export const rootPath = ''; 6 | export const completedTodoPath = 'completed-todos'; 7 | 8 | const appRoutes: Routes = [ 9 | { 10 | path: rootPath, 11 | component: TodoListComponent, 12 | pathMatch: 'full' 13 | }, 14 | { 15 | path: completedTodoPath, 16 | loadChildren: './todo-list-completed/todo-list-completed.module#TodoListCompletedModule' 17 | } 18 | ]; 19 | 20 | export const appRouterModule = RouterModule.forRoot(appRoutes); 21 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { OverlayModule } from '@angular/cdk/overlay'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { SpinnerOverlayModule } from '@app/core/spinner-overlay/spinner-overlay.module'; 5 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 6 | import { StateModule } from './store/store.module'; 7 | 8 | @NgModule({ 9 | imports: [OverlayModule, SpinnerOverlayModule, StateModule], 10 | declarations: [], 11 | providers: [TodoListService] 12 | }) 13 | export class CoreModule {} 14 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.component.scss: -------------------------------------------------------------------------------- 1 | .spinner-wrapper { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | top: 0; 9 | left: 0; 10 | background-color: rgba(255, 255, 255, 0.5); 11 | z-index: 998; 12 | app-spinner { 13 | width: 6rem; 14 | height: 6rem; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { SpinnerOverlayComponent } from '@app/core/spinner-overlay/spinner-overlay.component'; 5 | import { SpinnerComponent } from '@shared-lib/spinner/spinner.component'; 6 | 7 | describe('SpinnerOverlayComponent', () => { 8 | let component: SpinnerOverlayComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [SpinnerOverlayComponent, SpinnerComponent] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(SpinnerOverlayComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-spinner-overlay', 5 | templateUrl: './spinner-overlay.component.html', 6 | styleUrls: ['./spinner-overlay.component.scss'] 7 | }) 8 | export class SpinnerOverlayComponent { 9 | @Input() public message: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { SpinnerOverlayComponent } from '@app/core/spinner-overlay/spinner-overlay.component'; 5 | import { SpinnerOverlayService } from '@app/core/spinner-overlay/spinner-overlay.service'; 6 | import { SpinnerModule } from '@shared-lib/spinner/spinner.module'; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, SpinnerModule], 10 | declarations: [SpinnerOverlayComponent], 11 | entryComponents: [SpinnerOverlayComponent], 12 | providers: [SpinnerOverlayService], 13 | exports: [] 14 | }) 15 | export class SpinnerOverlayModule {} 16 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { Overlay } from '@angular/cdk/overlay'; 4 | import { inject, TestBed } from '@angular/core/testing'; 5 | 6 | import { SpinnerOverlayService } from '@app/core/spinner-overlay/spinner-overlay.service'; 7 | 8 | describe('Service: SpinnerOverlay', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ 11 | providers: [SpinnerOverlayService, Overlay] 12 | }); 13 | }); 14 | 15 | it('should ...', inject([SpinnerOverlayService], (service: SpinnerOverlayService) => { 16 | expect(service).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/core/spinner-overlay/spinner-overlay.service.ts: -------------------------------------------------------------------------------- 1 | import { Overlay, OverlayRef } from '@angular/cdk/overlay'; 2 | import { ComponentPortal } from '@angular/cdk/portal'; 3 | import { Injectable } from '@angular/core'; 4 | 5 | import { SpinnerOverlayComponent } from '@app/core/spinner-overlay/spinner-overlay.component'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class SpinnerOverlayService { 11 | private overlayRef: OverlayRef = null; 12 | 13 | constructor(private overlay: Overlay) {} 14 | 15 | public show(message = '') { 16 | // Returns an OverlayRef (which is a PortalHost) 17 | 18 | if (!this.overlayRef) { 19 | this.overlayRef = this.overlay.create(); 20 | } 21 | 22 | // Create ComponentPortal that can be attached to a PortalHost 23 | const spinnerOverlayPortal = new ComponentPortal(SpinnerOverlayComponent); 24 | 25 | // run in async context for triggering "tick", thus avoid ExpressionChangedAfterItHasBeenCheckedError 26 | setTimeout(() => { 27 | const component = this.overlayRef.attach(spinnerOverlayPortal); // Attach ComponentPortal to PortalHost 28 | 29 | // TODO: set message 30 | // component.instance.message = message; 31 | }); 32 | } 33 | 34 | public hide() { 35 | if (!!this.overlayRef) { 36 | this.overlayRef.detach(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/core/store/generic-action.ts: -------------------------------------------------------------------------------- 1 | export class GenericAction { 2 | 3 | public type: ActionType; 4 | public payload?: PayloadType; 5 | constructor(type: ActionType, payload?: PayloadType) { 6 | this.type = type; 7 | this.payload = payload; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/core/store/store.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { StoreModule } from '@ngrx/store'; 5 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 6 | import { environment } from 'environments/environment'; 7 | 8 | import { TodoListStoreModule } from '../todo-list/redux-api/todo-list-store.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | StoreModule.forRoot({}), 14 | EffectsModule.forRoot([]), 15 | TodoListStoreModule, 16 | StoreDevtoolsModule.instrument({ 17 | name: 'NgRx Testing Store DevTools', 18 | logOnly: environment.production 19 | }) 20 | ], 21 | declarations: [] 22 | }) 23 | export class StateModule { 24 | constructor( 25 | @Optional() 26 | @SkipSelf() 27 | parentModule?: StateModule 28 | ) { 29 | if (parentModule) { 30 | throw new Error('StateModule is already loaded. Import it in the AppModule only'); 31 | } 32 | } 33 | public static forRoot(): ModuleWithProviders { 34 | return { 35 | ngModule: StateModule 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list-store.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { EffectsModule } from '@ngrx/effects'; 3 | import { StoreModule } from '@ngrx/store'; 4 | 5 | import { TodoListEffects } from './todo-list.effects'; 6 | import { todoListReducers } from './todo-list.reducers'; 7 | import { TodoListSelector } from './todo-list.selector'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | StoreModule.forFeature('todoList', todoListReducers), 12 | EffectsModule.forFeature([TodoListEffects]) 13 | ], 14 | providers: [TodoListSelector] 15 | }) 16 | export class TodoListStoreModule {} 17 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.actions.spec.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | 3 | import { LoadTodoList, TodoListActions } from './todo-list.actions'; 4 | import { TodoListState } from './todo-list.model'; 5 | 6 | describe('Todo list actions', () => { 7 | describe('loadTodoList', () => { 8 | it('should dispatch load todolist action', () => { 9 | const expectedAction = new LoadTodoList(); 10 | const store = jasmine.createSpyObj>('store', ['dispatch']); 11 | 12 | const todoListActions = new TodoListActions(store); 13 | todoListActions.loadTodoList(); 14 | 15 | expect(store.dispatch).toHaveBeenCalledWith(expectedAction); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.actions.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Action, Store } from '@ngrx/store'; 3 | 4 | import { TODOItem } from '@app/shared/models/todo-item'; 5 | import { TodoListState } from './todo-list.model'; 6 | 7 | export enum TodoListActionTypes { 8 | LoadTodoList = '[TodoList] Load Todo List', 9 | TodoItemsLoaded = '[TodoList] TodoItemsLoaded', 10 | TodoItemsLoadFailed = '[TodoList] load todo items failed', 11 | TodoItemCreated = '[TodoList] TodoItemCreated', 12 | TodoItemDeleted = '[TodoList] TodoItemDeleted', 13 | TodoItemUpdated = '[TodoList] TodoItemUpdated', 14 | TodoItemCompleted = '[TodoList] TodoItemCompleted', 15 | SetTodoItemForEdit = '[TodoList] SetTodoItemForEdit' 16 | } 17 | 18 | export class LoadTodoList implements Action { 19 | public readonly type = TodoListActionTypes.LoadTodoList; 20 | 21 | constructor() {} 22 | } 23 | 24 | export class TodoItemsLoaded implements Action { 25 | public readonly type = TodoListActionTypes.TodoItemsLoaded; 26 | 27 | constructor(public payload: TODOItem[]) {} 28 | } 29 | 30 | export class TodoItemsLoadFailed implements Action { 31 | public readonly type = TodoListActionTypes.TodoItemsLoadFailed; 32 | 33 | constructor(public payload: Error) {} 34 | } 35 | 36 | export class AddTodoItemAction implements Action { 37 | public readonly type = TodoListActionTypes.TodoItemCreated; 38 | 39 | constructor(public payload: TODOItem) {} 40 | } 41 | 42 | export class TodoItemDeleted implements Action { 43 | public readonly type = TodoListActionTypes.TodoItemDeleted; 44 | 45 | constructor(public payload: string) {} 46 | } 47 | 48 | export class TodoItemUpdated implements Action { 49 | public readonly type = TodoListActionTypes.TodoItemUpdated; 50 | 51 | constructor(public payload: TODOItem) {} 52 | } 53 | 54 | export class SetTodoItemForEditAction implements Action { 55 | public readonly type = TodoListActionTypes.SetTodoItemForEdit; 56 | 57 | constructor(public payload: TODOItem) {} 58 | } 59 | 60 | export class TodoItemCompleted implements Action { 61 | public readonly type = TodoListActionTypes.TodoItemCompleted; 62 | 63 | constructor(public payload: string) {} 64 | } 65 | 66 | @Injectable({ providedIn: 'root' }) 67 | export class TodoListActions { 68 | constructor(private store: Store) {} 69 | 70 | public loadTodoList(): void { 71 | this.store.dispatch(new LoadTodoList()); 72 | } 73 | 74 | public addTodo(todo: TODOItem): any { 75 | this.store.dispatch(new AddTodoItemAction(todo)); 76 | } 77 | 78 | public todoItemUpdated(todoItem: TODOItem): any { 79 | this.store.dispatch(new TodoItemUpdated(todoItem)); 80 | } 81 | 82 | public setTodoItemForEdit(todoItem: TODOItem): any { 83 | this.store.dispatch(new SetTodoItemForEditAction(todoItem)); 84 | } 85 | 86 | public deleteTodo(id: string) { 87 | this.store.dispatch(new TodoItemDeleted(id)); 88 | } 89 | 90 | public todoItemCompleted(id: string) { 91 | this.store.dispatch(new TodoItemCompleted(id)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { provideMockActions } from '@ngrx/effects/testing'; 3 | import { cold, hot } from 'jasmine-marbles'; 4 | import { Observable } from 'rxjs'; 5 | 6 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 7 | import { TODOItem } from '@app/shared/models/todo-item'; 8 | import { LoadTodoList, TodoItemsLoaded, TodoItemsLoadFailed } from './todo-list.actions'; 9 | import { TodoListEffects } from './todo-list.effects'; 10 | 11 | describe('TodoListEffects', () => { 12 | let actions: Observable; 13 | 14 | let effects: TodoListEffects; 15 | let todoListService: jasmine.SpyObj; 16 | 17 | beforeEach(() => { 18 | TestBed.configureTestingModule({ 19 | providers: [ 20 | TodoListEffects, 21 | provideMockActions(() => actions), 22 | { 23 | provide: TodoListService, 24 | useValue: { 25 | getTodos: jasmine.createSpy() 26 | } 27 | } 28 | ] 29 | }); 30 | 31 | effects = TestBed.get(TodoListEffects); 32 | todoListService = TestBed.get(TodoListService); 33 | }); 34 | 35 | describe('loadTodoList', () => { 36 | it('should return a stream with todo list loaded action', () => { 37 | const todoList: TODOItem[] = [{ title: '', id: '1', description: '' }]; 38 | const action = new LoadTodoList(); 39 | const outcome = new TodoItemsLoaded(todoList); 40 | 41 | actions = hot('-a', { a: action }); 42 | const response = cold('-a|', { a: todoList }); 43 | todoListService.getTodos.and.returnValue(response); 44 | 45 | const expected = cold('--b', { b: outcome }); 46 | expect(effects.loadTodoList$).toBeObservable(expected); 47 | }); 48 | 49 | it('should fail and return an action with the error', () => { 50 | const action = new LoadTodoList(); 51 | const error = new Error('some error') as any; 52 | const outcome = new TodoItemsLoadFailed(error); 53 | 54 | actions = hot('-a', { a: action }); 55 | const response = cold('-#|', {}, error); 56 | todoListService.getTodos.and.returnValue(response); 57 | 58 | const expected = cold('--(b|)', { b: outcome }); 59 | expect(effects.loadTodoList$).toBeObservable(expected); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions, Effect, ofType } from '@ngrx/effects'; 3 | import { of } from 'rxjs'; 4 | import { catchError, exhaustMap, map } from 'rxjs/operators'; 5 | 6 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 7 | import { TodoItemsLoaded, TodoItemsLoadFailed, TodoListActionTypes } from './todo-list.actions'; 8 | 9 | @Injectable() 10 | export class TodoListEffects { 11 | @Effect() 12 | public loadTodoList$ = this.actions$.pipe( 13 | ofType(TodoListActionTypes.LoadTodoList), 14 | exhaustMap(() => this.todoListService.getTodos()), 15 | map((todoList) => new TodoItemsLoaded(todoList)), 16 | catchError((error: Error) => of(new TodoItemsLoadFailed(error))) 17 | ); 18 | constructor(private actions$: Actions, private todoListService: TodoListService) {} 19 | } 20 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.model.ts: -------------------------------------------------------------------------------- 1 | import { TODOItem } from '@app/shared/models/todo-item'; 2 | 3 | export interface TodoListState { 4 | todos: TODOItem[]; 5 | errors?: Error; 6 | isLoading: boolean; 7 | editTodoItemIdx?: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.reducers.spec.ts: -------------------------------------------------------------------------------- 1 | import { GenericAction } from '@app/core/store/generic-action'; 2 | import { TODOItem } from '@app/shared/models/todo-item'; 3 | import { TodoListActionTypes } from './todo-list.actions'; 4 | import { TodoListInitState, todoListReducers } from './todo-list.reducers'; 5 | 6 | describe('TodoList reducer', () => { 7 | describe('default', () => { 8 | it('should return init state', () => { 9 | const noopAction = new GenericAction('noop' as TodoListActionTypes); 10 | const newState = todoListReducers(undefined, noopAction); 11 | 12 | const initState = new TodoListInitState(); 13 | expect(newState).toEqual(initState); 14 | }); 15 | }); 16 | 17 | describe('loadTodoItems', () => { 18 | it('should return isLoading true', () => { 19 | const initState = new TodoListInitState(); 20 | const loadTodoItemsAction = new GenericAction(TodoListActionTypes.LoadTodoList); 21 | const newState = todoListReducers(initState, loadTodoItemsAction); 22 | 23 | expect(newState.isLoading).toBe(true); 24 | }); 25 | }); 26 | 27 | describe('todoItemsLoadFailed', () => { 28 | it('should return isLoading false and error', () => { 29 | const initState = new TodoListInitState(); 30 | const error = new Error('http error'); 31 | const loadTodoItemsAction = new GenericAction(TodoListActionTypes.TodoItemsLoadFailed, error); 32 | const newState = todoListReducers(initState, loadTodoItemsAction); 33 | 34 | expect(newState.isLoading).toBe(false); 35 | expect(newState.errors).toBe(error); 36 | }); 37 | }); 38 | 39 | describe('todoItemCreatedReducer', () => { 40 | it('should add new todo to todo list', () => { 41 | const initState = new TodoListInitState(); 42 | const newTodo = new TODOItem('new todo', 'this is new'); 43 | const loadTodoItemsAction = new GenericAction(TodoListActionTypes.TodoItemCreated, newTodo); 44 | const newState = todoListReducers(initState, loadTodoItemsAction); 45 | 46 | expect(newState.todos.length).toBe(1); 47 | expect(newState.todos[0]).toEqual(newTodo); 48 | }); 49 | }); 50 | 51 | describe('todoItemDeletedReducer', () => { 52 | it('should delete todo from todo list', () => { 53 | const initState = new TodoListInitState(); 54 | initState.todos = [new TODOItem('todoToDelete', '')]; 55 | 56 | expect(initState.todos.length).toBe(1); 57 | 58 | const todoToDelete = initState.todos[0].id; 59 | const action = new GenericAction(TodoListActionTypes.TodoItemDeleted, todoToDelete); 60 | const newState = todoListReducers(initState, action); 61 | 62 | expect(newState.todos.length).toBe(0); 63 | }); 64 | }); 65 | 66 | describe('todoItemUpdatedReducer', () => { 67 | it('should update todo item', () => { 68 | const initState = new TodoListInitState(); 69 | const oldTodoItem = new TODOItem('todoToUpdate', ''); 70 | oldTodoItem.id = 'todoToUpdate'; 71 | initState.todos = [oldTodoItem]; 72 | 73 | expect(initState.todos.length).toBe(1); 74 | 75 | const updatedTodo = new TODOItem('todoToUpdate', 'new msg'); 76 | updatedTodo.id = oldTodoItem.id; 77 | const loadTodoItemsAction = new GenericAction( 78 | TodoListActionTypes.TodoItemUpdated, 79 | updatedTodo 80 | ); 81 | const newState = todoListReducers(initState, loadTodoItemsAction); 82 | 83 | expect(newState.todos[0]).toBe(updatedTodo); 84 | }); 85 | }); 86 | 87 | describe('todoItemCompletedReducer', () => { 88 | it('should complete given todo item', () => { 89 | const initState = new TodoListInitState(); 90 | const oldTodoItem = new TODOItem('todoToUpdate', ''); 91 | oldTodoItem.id = 'todoToUpdate'; 92 | oldTodoItem.completed = false; 93 | initState.todos = [oldTodoItem]; 94 | 95 | expect(initState.todos.length).toBe(1); 96 | expect(initState.todos[0].completed).toBe(false); 97 | 98 | const loadTodoItemsAction = new GenericAction( 99 | TodoListActionTypes.TodoItemCompleted, 100 | oldTodoItem.id 101 | ); 102 | const newState = todoListReducers(initState, loadTodoItemsAction); 103 | 104 | expect(newState.todos[0].completed).toBe(true); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.reducers.ts: -------------------------------------------------------------------------------- 1 | import { GenericAction } from '@app/core/store/generic-action'; 2 | import { Guid } from '@app/shared/models/guid'; 3 | import { TODOItem } from '@app/shared/models/todo-item'; 4 | import { TodoListActionTypes } from './todo-list.actions'; 5 | import { TodoListState } from './todo-list.model'; 6 | 7 | export class TodoListInitState implements TodoListState { 8 | public todos: TODOItem[]; 9 | public errors?: Error; 10 | public isLoading: boolean; 11 | constructor() { 12 | this.todos = []; 13 | this.isLoading = false; 14 | } 15 | } 16 | 17 | const loadTodoItems = ( 18 | lastState: TodoListState, 19 | action: GenericAction 20 | ): TodoListState => { 21 | return { 22 | ...lastState, 23 | isLoading: true 24 | }; 25 | }; 26 | 27 | const todoItemsLoaded = ( 28 | lastState: TodoListState, 29 | action: GenericAction 30 | ): TodoListState => { 31 | return { 32 | ...lastState, 33 | todos: action.payload, 34 | isLoading: false 35 | }; 36 | }; 37 | 38 | const todoItemsLoadFailed = ( 39 | lastState: TodoListState, 40 | action: GenericAction 41 | ): TodoListState => { 42 | return { 43 | ...lastState, 44 | errors: action.payload, 45 | isLoading: false 46 | }; 47 | }; 48 | 49 | const todoItemCreatedReducer = ( 50 | lastState: TodoListState, 51 | action: GenericAction 52 | ): TodoListState => { 53 | const prevTodos = lastState.todos; 54 | 55 | action.payload.id = Guid.newGuid(); 56 | prevTodos.push(action.payload); 57 | const newTodos = prevTodos; 58 | return { 59 | ...lastState, 60 | todos: newTodos 61 | }; 62 | }; 63 | 64 | const selectTodoItemForEditReducer = ( 65 | lastState: TodoListState, 66 | action: GenericAction 67 | ): TodoListState => { 68 | const indexToUpdate = lastState.todos.findIndex((todo) => todo.id === action.payload.id); 69 | return { 70 | ...lastState, 71 | editTodoItemIdx: indexToUpdate 72 | }; 73 | }; 74 | 75 | const todoItemUpdatedReducer = ( 76 | lastState: TodoListState, 77 | action: GenericAction 78 | ): TodoListState => { 79 | const newTodolist = lastState.todos.map((todo) => 80 | todo.id === action.payload.id ? action.payload : todo 81 | ); 82 | 83 | return { 84 | ...lastState, 85 | editTodoItemIdx: null, 86 | todos: newTodolist 87 | }; 88 | }; 89 | 90 | const todoItemDeletedReducer = ( 91 | lastState: TodoListState, 92 | action: GenericAction 93 | ): TodoListState => { 94 | const newState = lastState.todos.filter((todo) => todo.id !== action.payload); 95 | 96 | return { 97 | ...lastState, 98 | editTodoItemIdx: null, 99 | todos: newState 100 | }; 101 | }; 102 | 103 | const todoItemCompletedReducer = ( 104 | lastState: TodoListState, 105 | action: GenericAction 106 | ) => { 107 | lastState.todos.find((todo) => todo.id === action.payload).completed = true; 108 | 109 | return { ...lastState }; 110 | }; 111 | 112 | export function todoListReducers( 113 | lastState: TodoListState = new TodoListInitState(), 114 | action: GenericAction 115 | ): TodoListState { 116 | switch (action.type) { 117 | case TodoListActionTypes.LoadTodoList: 118 | return loadTodoItems(lastState, action); 119 | case TodoListActionTypes.TodoItemsLoaded: 120 | return todoItemsLoaded(lastState, action); 121 | case TodoListActionTypes.TodoItemsLoadFailed: 122 | return todoItemsLoadFailed(lastState, action); 123 | case TodoListActionTypes.TodoItemCreated: 124 | return todoItemCreatedReducer(lastState, action); 125 | case TodoListActionTypes.TodoItemsLoadFailed: 126 | return todoItemsLoadFailed(lastState, action); 127 | case TodoListActionTypes.SetTodoItemForEdit: 128 | return selectTodoItemForEditReducer(lastState, action); 129 | case TodoListActionTypes.TodoItemDeleted: 130 | return todoItemDeletedReducer(lastState, action); 131 | case TodoListActionTypes.TodoItemUpdated: 132 | return todoItemUpdatedReducer(lastState, action); 133 | case TodoListActionTypes.TodoItemCompleted: 134 | return todoItemCompletedReducer(lastState, action); 135 | 136 | default: 137 | return lastState; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.selector.spec.ts: -------------------------------------------------------------------------------- 1 | import { Store } from '@ngrx/store'; 2 | 3 | import { TODOItem } from '@app/shared/models/todo-item'; 4 | import { TodoListState } from './todo-list.model'; 5 | import { TodoListSelector, todoListSelectorFn } from './todo-list.selector'; 6 | 7 | describe('TodoListSelector', () => { 8 | describe('getTodoList', () => { 9 | it('should return the todoList', () => { 10 | const todos = [new TODOItem('todo1', 'todo1')]; 11 | 12 | const todoListState = { 13 | todos, 14 | isLoading: true 15 | } as TodoListState; 16 | 17 | expect(todoListSelectorFn.projector(todoListState)).toEqual(todos); 18 | }); 19 | 20 | it('should return call the todoListSelectorFn', () => { 21 | const store = jasmine.createSpyObj>('store', ['select']); 22 | 23 | const todoListSelector = new TodoListSelector(store); 24 | todoListSelector.getTodoList$(); 25 | 26 | expect(store.select).toHaveBeenCalledWith(todoListSelectorFn); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/core/todo-list/redux-api/todo-list.selector.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { createFeatureSelector, createSelector, Store } from '@ngrx/store'; 3 | 4 | import { TodoListState } from './todo-list.model'; 5 | 6 | export const getTodolistState = createFeatureSelector('todoList'); 7 | 8 | export const todoListSelectorFn = createSelector( 9 | getTodolistState, 10 | (todoListState) => todoListState.todos 11 | ); 12 | 13 | export const completedTodosSelectorFn = createSelector( 14 | todoListSelectorFn, 15 | (todos) => todos.filter((todo) => todo.completed) 16 | ); 17 | 18 | export const todoItemForEditSelectorFn = createSelector( 19 | getTodolistState, 20 | (todoListState) => todoListState.todos[todoListState.editTodoItemIdx] 21 | ); 22 | 23 | export const isLoadingFn = createSelector( 24 | getTodolistState, 25 | (todoListState) => todoListState.isLoading 26 | ); 27 | 28 | @Injectable({ 29 | providedIn: 'root' 30 | }) 31 | export class TodoListSelector { 32 | constructor(private store: Store) {} 33 | /** 34 | * getTodoList 35 | */ 36 | public getTodoList$() { 37 | return this.store.select(todoListSelectorFn); 38 | } 39 | 40 | public getCompletedTodoList$() { 41 | return this.store.select(completedTodosSelectorFn); 42 | } 43 | 44 | public getTodoItemForEdit$() { 45 | return this.store.select(todoItemForEditSelectorFn); 46 | } 47 | 48 | public getIsLoading$() { 49 | return this.store.select(isLoadingFn); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/core/todo-list/todo-list.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { HttpClient } from '@angular/common/http'; 4 | import { of } from 'rxjs'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 8 | import { createMagicalMock } from '@app/helpers/spy-helper'; 9 | import { TODOItem } from '@app/shared/models/todo-item'; 10 | import { TodoListActions } from './redux-api/todo-list.actions'; 11 | import { TodoListSelector } from './redux-api/todo-list.selector'; 12 | 13 | describe('Service: TodoList', () => { 14 | let todoListService: TodoListService; 15 | const httpClientSpy: jasmine.SpyObj = jasmine.createSpyObj('httpClient', ['get']); 16 | const todoList$ = of([new TODOItem('Buy Milk', 'Lala')]); 17 | httpClientSpy.get.and.returnValue(todoList$); 18 | 19 | const todoListSelectorStub = createMagicalMock(TodoListSelector); 20 | todoListSelectorStub.getTodoList$.and.returnValue(todoList$); 21 | const todoListActionsStub = createMagicalMock(TodoListActions); 22 | 23 | beforeEach(() => { 24 | todoListService = new TodoListService(httpClientSpy, todoListSelectorStub, todoListActionsStub); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(todoListService).toBeTruthy(); 29 | }); 30 | 31 | it('should make a http get request', (done) => { 32 | todoListService.getTodos().subscribe(); 33 | 34 | todoListService.todoList$.pipe(first()).subscribe((todoList) => { 35 | expect(httpClientSpy.get).toHaveBeenCalled(); 36 | expect(todoList.length).toBe(1); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/core/todo-list/todo-list.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | import { TODOItem } from '@app/shared/models/todo-item'; 5 | import { TodoListActions } from './redux-api/todo-list.actions'; 6 | import { TodoListSelector } from './redux-api/todo-list.selector'; 7 | 8 | @Injectable() 9 | export class TodoListService { 10 | public isLoading$ = this.todoListSelector.getIsLoading$(); 11 | 12 | public todoList$ = this.todoListSelector.getTodoList$(); 13 | public completedTodoList$ = this.todoListSelector.getCompletedTodoList$(); 14 | 15 | private todoListUrl = '//localhost:8080/api/todo-list'; 16 | 17 | constructor( 18 | private httpClient: HttpClient, 19 | private todoListSelector: TodoListSelector, 20 | private todoListActions: TodoListActions 21 | ) {} 22 | 23 | public getTodos() { 24 | return this.httpClient.get(this.todoListUrl); 25 | } 26 | 27 | public loadTodoList(): any { 28 | this.todoListActions.loadTodoList(); 29 | } 30 | 31 | public setTodoItemForEdit(todoItem: TODOItem): any { 32 | this.todoListActions.setTodoItemForEdit(todoItem); 33 | } 34 | 35 | public editTodo(todoItem: TODOItem): any { 36 | this.todoListActions.todoItemUpdated(todoItem); 37 | } 38 | 39 | public getTodoForEdit$() { 40 | return this.todoListSelector.getTodoItemForEdit$(); 41 | } 42 | 43 | public addTodo(todo: TODOItem) { 44 | this.todoListActions.addTodo(todo); 45 | } 46 | 47 | public deleteTodo(id: string) { 48 | this.todoListActions.deleteTodo(id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/app/footer/footer.component.css -------------------------------------------------------------------------------- /src/app/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Back to top 6 |

7 |

TODO app 2018

8 |
9 |
-------------------------------------------------------------------------------- /src/app/footer/footer.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | template: '' 6 | }) 7 | // tslint:disable-next-line:component-class-suffix 8 | export class FooterComponentMock {} 9 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { FooterComponent } from '@app/footer/footer.component'; 5 | 6 | describe('FooterComponent', () => { 7 | let component: FooterComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [FooterComponent] 13 | }).compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.css'] 7 | }) 8 | export class FooterComponent {} 9 | -------------------------------------------------------------------------------- /src/app/helpers/spy-helper.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Type } from '@angular/core'; 2 | 3 | export type Mock = T & { [P in keyof T]: T[P] & jasmine.Spy }; 4 | 5 | export function createMagicalMock(type: Type): Mock { 6 | const target: any = {}; 7 | 8 | function installProtoMethods(proto: any) { 9 | if (proto === null || proto === Object.prototype) { 10 | return; 11 | } 12 | 13 | for (const key of Object.getOwnPropertyNames(proto)) { 14 | // tslint:disable-next-line: no-non-null-assertion 15 | const descriptor = Object.getOwnPropertyDescriptor(proto, key)!; 16 | 17 | if (typeof descriptor.value === 'function' && key !== 'constructor') { 18 | target[key] = jasmine.createSpy(key); 19 | } 20 | } 21 | 22 | installProtoMethods(Object.getPrototypeOf(proto)); 23 | } 24 | 25 | installProtoMethods(type.prototype); 26 | 27 | return target; 28 | } 29 | 30 | export function provideMagicalMock(type: Type): Provider { 31 | return { 32 | provide: type, 33 | useFactory: () => createMagicalMock(type) 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{'app-title' | translate }}
3 | 18 |
-------------------------------------------------------------------------------- /src/app/navbar/navbar.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-navbar', 5 | template: '' 6 | }) 7 | // tslint:disable-next-line:component-class-suffix 8 | export class NavbarComponentMock implements OnInit { 9 | constructor() {} 10 | 11 | public ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.scss: -------------------------------------------------------------------------------- 1 | .nav-active { 2 | background-color: #dae0e5; 3 | } 4 | 5 | a:hover { 6 | background-color: #eef4f9 7 | } -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { APP_BASE_HREF } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 4 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { BrowserModule } from '@angular/platform-browser'; 7 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 8 | import { TranslateModule } from '@ngx-translate/core'; 9 | 10 | import { AppComponent } from '@app/app.component'; 11 | import { appRouterModule } from '@app/app.routes'; 12 | import { CoreModule } from '@app/core/core.module'; 13 | import { FooterComponent } from '@app/footer/footer.component'; 14 | import { NavbarComponent } from '@app/navbar/navbar.component'; 15 | import { TodoItemListRowComponent } from '@app/shared/todo-item-list-row/todo-item-list-row.component'; 16 | import { TodoListCompletedComponent } from '@app/todo-list-completed/todo-list-completed.component'; 17 | import { AddTodoComponent } from '@app/todo-list/add-todo/add-todo.component'; 18 | import { DuedateTodayCountPipe } from '@app/todo-list/duedate-today-count.pipe'; 19 | import { TodoListComponent } from '@app/todo-list/todo-list.component'; 20 | 21 | describe('NavbarComponent', () => { 22 | let component: NavbarComponent; 23 | let fixture: ComponentFixture; 24 | 25 | beforeEach(async(() => { 26 | TestBed.configureTestingModule({ 27 | declarations: [ 28 | AppComponent, 29 | NavbarComponent, 30 | TodoListComponent, 31 | TodoItemListRowComponent, 32 | FooterComponent, 33 | AddTodoComponent, 34 | TodoListCompletedComponent, 35 | DuedateTodayCountPipe 36 | ], 37 | imports: [ 38 | BrowserModule, 39 | NgbModule, 40 | TranslateModule.forRoot(), 41 | FormsModule, 42 | CoreModule, 43 | HttpClientModule, 44 | appRouterModule 45 | ], 46 | providers: [{ provide: APP_BASE_HREF, useValue: '/' }], 47 | schemas: [NO_ERRORS_SCHEMA] 48 | }).compileComponents(); 49 | })); 50 | 51 | beforeEach(() => { 52 | fixture = TestBed.createComponent(NavbarComponent); 53 | component = fixture.componentInstance; 54 | fixture.detectChanges(); 55 | }); 56 | 57 | it('should create', () => { 58 | expect(component).toBeTruthy(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/app/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | 4 | @Component({ 5 | selector: 'app-navbar', 6 | templateUrl: './navbar.component.html', 7 | styleUrls: ['./navbar.component.scss'] 8 | }) 9 | export class NavbarComponent implements OnInit { 10 | constructor(public translate: TranslateService) {} 11 | 12 | public ngOnInit() {} 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/app-material/app-material.module.ts: -------------------------------------------------------------------------------- 1 | import { DragDropModule } from '@angular/cdk/drag-drop'; 2 | import { ScrollingModule } from '@angular/cdk/scrolling'; 3 | import { CdkTableModule } from '@angular/cdk/table'; 4 | import { CdkTreeModule } from '@angular/cdk/tree'; 5 | import { NgModule } from '@angular/core'; 6 | import { 7 | MatAutocompleteModule, 8 | MatBadgeModule, 9 | MatBottomSheetModule, 10 | MatButtonModule, 11 | MatButtonToggleModule, 12 | MatCardModule, 13 | MatCheckboxModule, 14 | MatChipsModule, 15 | MatDatepickerModule, 16 | MatDialogModule, 17 | MatDividerModule, 18 | MatExpansionModule, 19 | MatGridListModule, 20 | MatIconModule, 21 | MatInputModule, 22 | MatListModule, 23 | MatMenuModule, 24 | MatNativeDateModule, 25 | MatPaginatorModule, 26 | MatProgressBarModule, 27 | MatProgressSpinnerModule, 28 | MatRadioModule, 29 | MatRippleModule, 30 | MatSelectModule, 31 | MatSidenavModule, 32 | MatSliderModule, 33 | MatSlideToggleModule, 34 | MatSnackBarModule, 35 | MatSortModule, 36 | MatStepperModule, 37 | MatTableModule, 38 | MatTabsModule, 39 | MatToolbarModule, 40 | MatTooltipModule, 41 | MatTreeModule 42 | } from '@angular/material'; 43 | 44 | @NgModule({ 45 | exports: [ 46 | CdkTableModule, 47 | CdkTreeModule, 48 | DragDropModule, 49 | MatAutocompleteModule, 50 | MatBadgeModule, 51 | MatBottomSheetModule, 52 | MatButtonModule, 53 | MatButtonToggleModule, 54 | MatCardModule, 55 | MatCheckboxModule, 56 | MatChipsModule, 57 | MatStepperModule, 58 | MatDatepickerModule, 59 | MatDialogModule, 60 | MatDividerModule, 61 | MatExpansionModule, 62 | MatGridListModule, 63 | MatIconModule, 64 | MatInputModule, 65 | MatListModule, 66 | MatMenuModule, 67 | MatNativeDateModule, 68 | MatPaginatorModule, 69 | MatProgressBarModule, 70 | MatProgressSpinnerModule, 71 | MatRadioModule, 72 | MatRippleModule, 73 | MatSelectModule, 74 | MatSidenavModule, 75 | MatSliderModule, 76 | MatSlideToggleModule, 77 | MatSnackBarModule, 78 | MatSortModule, 79 | MatTableModule, 80 | MatTabsModule, 81 | MatToolbarModule, 82 | MatTooltipModule, 83 | MatTreeModule, 84 | ScrollingModule 85 | ] 86 | }) 87 | export class AppMaterialModule {} 88 | 89 | /** Copyright 2018 Google Inc. All Rights Reserved. 90 | Use of this source code is governed by an MIT-style license that 91 | can be found in the LICENSE file at http://angular.io/license */ 92 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards-list.component.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-cards-list', 5 | templateUrl: './cards-list.component.html', 6 | styleUrls: ['./cards-list.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class CardsTableComponent implements OnInit { 10 | @Input() public data: any; 11 | @Input() public cardRef: any; 12 | @Input() public tableRef: any; 13 | 14 | private preferedShowModeKey = 'typeToShow'; 15 | public get typeToShow(): string { 16 | return window.localStorage.getItem(this.preferedShowModeKey) || 'table'; 17 | } 18 | public set typeToShow(showMode: string) { 19 | window.localStorage.setItem(this.preferedShowModeKey, showMode); 20 | } 21 | 22 | constructor() {} 23 | 24 | public ngOnInit() {} 25 | 26 | public showCards() { 27 | this.typeToShow = 'cards'; 28 | } 29 | 30 | public showTable() { 31 | this.typeToShow = 'table'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards-list.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { AppMaterialModule } from '../app-material/app-material.module'; 6 | import { CardsTableComponent } from './cards-list.component'; 7 | import { CardsComponent } from './cards/cards.component'; 8 | import { ListComponent } from './list/list.component'; 9 | 10 | @NgModule({ 11 | imports: [CommonModule, AppMaterialModule, TranslateModule], 12 | declarations: [CardsTableComponent, ListComponent, CardsComponent], 13 | exports: [CardsTableComponent] 14 | }) 15 | export class CardListModule {} 16 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards/cards.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 | {{'taskCards.noData' | translate}} 11 |
12 |
13 | 14 |
15 | {{'taskCards.noCardRef' | translate}} 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards/cards.component.scss: -------------------------------------------------------------------------------- 1 | .cards-wrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | flex-direction: row; 5 | } 6 | 7 | .card-item { 8 | min-width: 280px; 9 | margin: 5px; 10 | flex-basis: 280px; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards/cards.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { CardsComponent } from './cards.component'; 5 | 6 | describe('CardsComponent', () => { 7 | let component: CardsComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [CardsComponent] 13 | }) 14 | .overrideTemplate(CardsComponent, '') 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(CardsComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/cards/cards.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-cards', 5 | templateUrl: './cards.component.html', 6 | styleUrls: ['./cards.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class CardsComponent implements OnInit { 10 | @Input() public cardRef: TemplateRef; 11 | @Input() public data: any; 12 | 13 | constructor() {} 14 | 15 | public ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/list/list.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/list/list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/app/shared/cards-list/list/list.component.scss -------------------------------------------------------------------------------- /src/app/shared/cards-list/list/list.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { ListComponent } from './list.component'; 5 | 6 | describe('TableComponent', () => { 7 | let component: ListComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ListComponent] 13 | }).compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/cards-list/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-list', 5 | templateUrl: './list.component.html', 6 | styleUrls: ['./list.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class ListComponent { 10 | @Input() public tableRef: TemplateRef; 11 | @Input() public data: any; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/invalid-date.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms'; 3 | 4 | export function InvalidDateValidator(): ValidatorFn { 5 | return (control: AbstractControl): { [key: string]: any } => { 6 | const date = new Date(control.value); 7 | const invalidDate = !control.value || date.getMonth === undefined; 8 | return invalidDate ? { invalidDate: { value: control.value } } : null; 9 | }; 10 | } 11 | 12 | @Directive({ 13 | selector: '[appInvalidDate]', 14 | providers: [{ provide: NG_VALIDATORS, useExisting: InvalidDateValidatorDirective, multi: true }] 15 | }) 16 | export class InvalidDateValidatorDirective implements Validator { 17 | // tslint:disable-next-line:no-input-rename 18 | @Input('appInvalidDate') public invalidDate: string; 19 | public validate(control: AbstractControl): { [key: string]: any } { 20 | return this.invalidDate ? InvalidDateValidator()(control) : null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/shared/models/guid.ts: -------------------------------------------------------------------------------- 1 | /*tslint:disable:no-bitwise*/ 2 | // Class for creating Guids: https://github.com/Steve-Fenton/TypeScriptUtilities/blob/master/Guid 3 | 4 | export class Guid { 5 | public static newGuid() { 6 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 7 | // tslint:disable-next-line:one-variable-per-declaration 8 | const r = Math.random() * 16, 9 | v = c === 'x' ? r : (r & 0x3) | 0x8; 10 | return v.toString(16); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/models/todo-item.ts: -------------------------------------------------------------------------------- 1 | export class TODOItem { 2 | 3 | public id: string; 4 | public title: string; 5 | public description: string; 6 | public dueDate?: string; 7 | public completed?: boolean; 8 | constructor(title: string, description: string, dueDate: string = null) { 9 | this.title = title; 10 | this.description = description; 11 | this.dueDate = dueDate; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | 5 | import { InvalidDateValidatorDirective } from '@app/shared/invalid-date.directive'; 6 | import { SpinnerOverlayWrapperModule } from '@app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.module'; 7 | import { SpinnerModule } from '@shared-lib/spinner/spinner.module'; 8 | import { AppMaterialModule } from './app-material/app-material.module'; 9 | import { CardListModule } from './cards-list/cards-list.module'; 10 | import { TodoItemCardComponent } from './todo-item-card/todo-item-card.component'; 11 | import { TodoItemListRowComponent } from './todo-item-list-row/todo-item-list-row.component'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | SpinnerModule, 17 | SpinnerOverlayWrapperModule, 18 | TranslateModule, 19 | CardListModule, 20 | AppMaterialModule 21 | ], 22 | declarations: [InvalidDateValidatorDirective, TodoItemListRowComponent, TodoItemCardComponent], 23 | exports: [ 24 | InvalidDateValidatorDirective, 25 | SpinnerModule, 26 | SpinnerOverlayWrapperModule, 27 | TranslateModule, 28 | CardListModule, 29 | TodoItemListRowComponent, 30 | TodoItemCardComponent, 31 | AppMaterialModule 32 | ] 33 | }) 34 | export class SharedModule {} 35 | -------------------------------------------------------------------------------- /src/app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 | 9 |
10 | -------------------------------------------------------------------------------- /src/app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .overlay { 7 | position: absolute; 8 | z-index: 1002; 9 | background-color: rgba(255, 255, 255, 0.5); 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | .spinner-wrapper { 15 | display: flex; 16 | justify-content: center; 17 | justify-items: center; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { SpinnerOverlayWrapperComponent } from '@app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component'; 5 | import { SpinnerModule } from '@shared-lib/spinner/spinner.module'; 6 | 7 | describe('SpinnerOverlayWrapperComponent', () => { 8 | let component: SpinnerOverlayWrapperComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [SpinnerOverlayWrapperComponent], 14 | imports: [SpinnerModule] 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(SpinnerOverlayWrapperComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-spinner-overlay-wrapper', 5 | templateUrl: './spinner-overlay-wrapper.component.html', 6 | styleUrls: ['./spinner-overlay-wrapper.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class SpinnerOverlayWrapperComponent {} 10 | -------------------------------------------------------------------------------- /src/app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { SpinnerOverlayWrapperComponent } from '@app/shared/spinner-overlay-wrapper/spinner-overlay-wrapper.component'; 4 | import { SpinnerModule } from '@shared-lib/spinner/spinner.module'; 5 | 6 | @NgModule({ 7 | imports: [SpinnerModule], 8 | declarations: [SpinnerOverlayWrapperComponent], 9 | exports: [SpinnerOverlayWrapperComponent] 10 | }) 11 | export class SpinnerOverlayWrapperModule {} 12 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-card/todo-item-card.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{todoItem.title}} 5 | {{todoItem.description}} 6 |
7 | 8 |

9 | {{'add-todo.due-date' | translate}}: 10 | {{todoItem.dueDate}} 11 | 12 |

13 |
14 | 15 | 18 | 21 | 24 | 25 |
26 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-card/todo-item-card.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { TODOItem } from '@app/shared/models/todo-item'; 3 | import { TodoItemCardComponent } from './todo-item-card.component'; 4 | 5 | @Component({ 6 | selector: 'app-todo-item-card', 7 | template: '' 8 | }) 9 | // tslint:disable-next-line:component-class-suffix 10 | export class TodoItemCardComponentMock implements OnInit, TodoItemCardComponent { 11 | @Input() public todoItem: TODOItem; 12 | @Input() public readOnlyTODO: boolean; 13 | @Output() public todoDelete = new EventEmitter(); 14 | @Output() public todoEdit = new EventEmitter(); 15 | 16 | constructor() {} 17 | 18 | public ngOnInit() {} 19 | 20 | public completeClick() { 21 | this.todoItem.completed = !this.todoItem.completed; 22 | } 23 | 24 | public deleteClick() { 25 | this.todoDelete.emit(this.todoItem.id); 26 | } 27 | 28 | public editClick() { 29 | this.todoEdit.emit(this.todoItem); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-card/todo-item-card.component.scss: -------------------------------------------------------------------------------- 1 | .bg-completed { 2 | background-color: #85cc95; 3 | } 4 | 5 | .card-content { 6 | padding: 0 20px; 7 | } 8 | 9 | .card-actions { 10 | display: flex; 11 | justify-content: space-around; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-card/todo-item-card.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { TodoItemCardComponent } from './todo-item-card.component'; 5 | 6 | describe('TodoItemCardComponent', () => { 7 | let component: TodoItemCardComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [TodoItemCardComponent], 13 | imports: [], 14 | providers: [] 15 | }) 16 | .overrideTemplate(TodoItemCardComponent, '') 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(TodoItemCardComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-card/todo-item-card.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { TODOItem } from '@app/shared/models/todo-item'; 4 | 5 | @Component({ 6 | selector: 'app-todo-item-card', 7 | templateUrl: './todo-item-card.component.html', 8 | styleUrls: ['./todo-item-card.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class TodoItemCardComponent { 12 | @Input() public todoItem: TODOItem; 13 | @Input() public readOnlyTODO: boolean; 14 | @Output() public todoDelete = new EventEmitter(); 15 | @Output() public todoEdit = new EventEmitter(); 16 | 17 | public completeClick() { 18 | this.todoItem.completed = !this.todoItem.completed; 19 | } 20 | 21 | public deleteClick() { 22 | this.todoDelete.emit(this.todoItem.id); 23 | } 24 | 25 | public editClick() { 26 | this.todoEdit.emit(this.todoItem); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-list-row/todo-item-list-row.component.html: -------------------------------------------------------------------------------- 1 |
2 |
  • 3 |
    4 |
    {{todoItem.title}}
    5 | {{todoItem.description}} 6 |
    7 | {{'add-todo.due-date' | translate}}: 8 | {{todoItem.dueDate}} 9 | 10 |
    11 |
    12 | 13 |
    14 | 17 | 20 | 23 |
    24 |
  • 25 |
    26 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-list-row/todo-item-list-row.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | 3 | import { TODOItem } from '@app/shared/models/todo-item'; 4 | import { TodoItemListRowComponent } from './todo-item-list-row.component'; 5 | 6 | @Component({ 7 | selector: 'app-todo-item-list-row', 8 | template: '' 9 | }) 10 | // tslint:disable-next-line:component-class-suffix 11 | export class TodoItemListRowComponentMock implements TodoItemListRowComponent, OnInit { 12 | public todoComplete: EventEmitter; 13 | @Input() public todoItem: TODOItem; 14 | @Input() public readOnlyTODO: boolean; 15 | @Output() public todoDelete = new EventEmitter(); 16 | @Output() public todoEdit = new EventEmitter(); 17 | 18 | public ngOnInit(): void {} 19 | 20 | public completeClick() { 21 | this.todoItem.completed = !this.todoItem.completed; 22 | } 23 | 24 | public deleteClick() { 25 | this.todoDelete.emit(this.todoItem.id); 26 | } 27 | 28 | public editClick() { 29 | this.todoEdit.emit(this.todoItem); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-list-row/todo-item-list-row.component.scss: -------------------------------------------------------------------------------- 1 | .bg-completed { 2 | background-color: #85cc95; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-list-row/todo-item-list-row.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TodoItemListRowComponent } from './todo-item-list-row.component'; 4 | 5 | /* tslint:disable:no-unused-variable */ 6 | 7 | describe('TodoItemListRowComponent', () => { 8 | let component: TodoItemListRowComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [TodoItemListRowComponent], 14 | imports: [] 15 | }) 16 | .overrideTemplate(TodoItemListRowComponent, '') 17 | .compileComponents(); 18 | })); 19 | 20 | beforeEach(() => { 21 | fixture = TestBed.createComponent(TodoItemListRowComponent); 22 | component = fixture.componentInstance; 23 | fixture.detectChanges(); 24 | }); 25 | 26 | it('should create', () => { 27 | expect(component).toBeTruthy(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/shared/todo-item-list-row/todo-item-list-row.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output 8 | } from '@angular/core'; 9 | 10 | import { TODOItem } from '@app/shared/models/todo-item'; 11 | 12 | @Component({ 13 | selector: 'app-todo-item-list-row', 14 | templateUrl: './todo-item-list-row.component.html', 15 | styleUrls: ['./todo-item-list-row.component.scss'], 16 | changeDetection: ChangeDetectionStrategy.OnPush 17 | }) 18 | export class TodoItemListRowComponent implements OnInit { 19 | @Input() public todoItem: TODOItem; 20 | @Input() public readOnlyTODO: boolean; 21 | @Output() public todoDelete = new EventEmitter(); 22 | @Output() public todoEdit = new EventEmitter(); 23 | @Output() public todoComplete = new EventEmitter(); 24 | 25 | constructor() {} 26 | 27 | public ngOnInit() {} 28 | 29 | public completeClick() { 30 | const newTodo = { 31 | ...this.todoItem, 32 | completed: !this.todoItem.completed 33 | }; 34 | 35 | this.todoComplete.emit(newTodo); 36 | } 37 | 38 | public deleteClick() { 39 | this.todoDelete.emit(this.todoItem.id); 40 | } 41 | 42 | public editClick() { 43 | this.todoEdit.emit(this.todoItem); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    {{'todo-list' | translate}}
    4 |
    5 |
      6 | 7 | 8 |
    9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/app/todo-list-completed/todo-list-completed.component.scss -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { APP_BASE_HREF } from '@angular/common'; 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { of } from 'rxjs'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { completedTodoPath } from '@app/app.routes'; 8 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 9 | import { TODOItem } from '@app/shared/models/todo-item'; 10 | import { TodoListCompletedComponent } from '@app/todo-list-completed/todo-list-completed.component'; 11 | 12 | describe('TodoListCompletedComponent', () => { 13 | let component: TodoListCompletedComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(async(() => { 17 | const todo1 = new TODOItem('Buy milk', 'Remember to buy milk'); 18 | todo1.completed = true; 19 | const todoList = [todo1]; 20 | 21 | TestBed.configureTestingModule({ 22 | declarations: [TodoListCompletedComponent], 23 | imports: [], 24 | providers: [ 25 | { provide: APP_BASE_HREF, useValue: completedTodoPath }, 26 | { 27 | provide: TodoListService, 28 | useValue: { 29 | completedTodoList$: of(todoList) 30 | } 31 | } 32 | ] 33 | }) 34 | .overrideTemplate(TodoListCompletedComponent, '') 35 | .compileComponents(); 36 | })); 37 | 38 | beforeEach(() => { 39 | fixture = TestBed.createComponent(TodoListCompletedComponent); 40 | component = fixture.componentInstance; 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should create', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | 48 | it('should have one completed TODO item', (done) => { 49 | component.completedTodoList$.pipe(first()).subscribe((todoList) => { 50 | expect(todoList.length).toBe(1); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 4 | 5 | @Component({ 6 | selector: 'app-todo-list-completed', 7 | templateUrl: './todo-list-completed.component.html', 8 | styleUrls: ['./todo-list-completed.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class TodoListCompletedComponent { 12 | public completedTodoList$ = this.todoListService.completedTodoList$; 13 | 14 | constructor(private todoListService: TodoListService) {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { SharedModule } from '@app/shared/shared.module'; 6 | import { TodoListCompletedComponent } from '@app/todo-list-completed/todo-list-completed.component'; 7 | import { TodoListCompletedRoutes } from './todo-list-completed.routing'; 8 | 9 | @NgModule({ 10 | imports: [FormsModule, CommonModule, SharedModule, TodoListCompletedRoutes], 11 | declarations: [TodoListCompletedComponent] 12 | }) 13 | export class TodoListCompletedModule {} 14 | -------------------------------------------------------------------------------- /src/app/todo-list-completed/todo-list-completed.routing.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule, Routes } from '@angular/router'; 2 | 3 | import { TodoListCompletedComponent } from './todo-list-completed.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: TodoListCompletedComponent 9 | } 10 | ]; 11 | 12 | export const TodoListCompletedRoutes = RouterModule.forChild(routes); 13 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo-presentation/add-todo-presentation.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    {{'add-todo.headline' | translate}}
    3 |
    4 | 5 | 8 |
    9 |
    10 | 11 | 13 |
    14 |
    15 | 16 | 18 |
    19 |
    20 | 21 | 23 |
    24 | 25 |
    26 | 27 |
    28 |
    29 |
    30 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo-presentation/add-todo-presentation.component.scss: -------------------------------------------------------------------------------- 1 | .relative { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo-presentation/add-todo-presentation.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { AddTodoPresentationComponent } from './add-todo-presentation.component'; 5 | 6 | describe('AddTodoPresentationComponent', () => { 7 | let component: AddTodoPresentationComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [AddTodoPresentationComponent] 13 | }) 14 | .overrideTemplate(AddTodoPresentationComponent, '') 15 | .compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(AddTodoPresentationComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo-presentation/add-todo-presentation.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output 8 | } from '@angular/core'; 9 | import { NgForm } from '@angular/forms'; 10 | 11 | import { TODOItem } from '@app/shared/models/todo-item'; 12 | 13 | @Component({ 14 | selector: 'app-add-todo-presentation', 15 | templateUrl: './add-todo-presentation.component.html', 16 | styleUrls: ['./add-todo-presentation.component.scss'], 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class AddTodoPresentationComponent implements OnInit { 20 | @Input() public currentTODO: TODOItem; 21 | 22 | @Input() public isLoading = false; 23 | 24 | @Output() public saved = new EventEmitter(); 25 | 26 | constructor() {} 27 | 28 | public ngOnInit() {} 29 | 30 | public save(form: NgForm) { 31 | this.saved.emit(form); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.component.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-add-todo', 5 | template: '' 6 | }) 7 | // tslint:disable-next-line:component-class-suffix 8 | export class AddTodoComponentMock { 9 | public isLoading = false; 10 | 11 | @Input() 12 | public currentTODO; 13 | 14 | public save() {} 15 | } 16 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 2 | /* tslint:disable:no-unused-variable */ 3 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { FormsModule, NgForm } from '@angular/forms'; 5 | import { TranslateModule } from '@ngx-translate/core'; 6 | import { of } from 'rxjs'; 7 | 8 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 9 | import { createMagicalMock, provideMagicalMock } from '@app/helpers/spy-helper'; 10 | import { AddTodoComponent } from './add-todo.component'; 11 | import { AddTodoService } from './add-todo.service'; 12 | 13 | describe('AddTodoComponent', () => { 14 | let component: AddTodoComponent; 15 | let fixture: ComponentFixture; 16 | 17 | const addTodoServiceStub = createMagicalMock(AddTodoService); 18 | 19 | beforeEach(async(() => { 20 | TestBed.configureTestingModule({ 21 | declarations: [AddTodoComponent], 22 | imports: [FormsModule, TranslateModule.forRoot()], 23 | providers: [provideMagicalMock(TodoListService)], 24 | schemas: [NO_ERRORS_SCHEMA] 25 | }) 26 | .overrideProvider(AddTodoService, { useValue: addTodoServiceStub }) 27 | .compileComponents(); 28 | })); 29 | 30 | let todoListServiceMock: jasmine.SpyObj; 31 | beforeEach(() => { 32 | todoListServiceMock = TestBed.get(TodoListService); 33 | 34 | fixture = TestBed.createComponent(AddTodoComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should create', () => { 40 | expect(component).toBeTruthy(); 41 | }); 42 | 43 | it('should update todo item when todo item is in todo list', () => { 44 | // Arrange 45 | const todoList = [ 46 | { id: 'task1', title: 'Buy Milk', description: 'Remember to buy milk' }, 47 | { id: 'task2', title: 'Go to the gym', description: 'Remember to work out' } 48 | ]; 49 | (todoListServiceMock as any).todoList = todoList; 50 | todoListServiceMock.editTodo.and.returnValue(of([])); 51 | 52 | // Act 53 | component.currentTODO = todoList[0]; 54 | const form = new NgForm([], []); 55 | component.save(form); 56 | 57 | // Assert 58 | expect(addTodoServiceStub.save).toHaveBeenCalledWith(form); 59 | }); 60 | 61 | it('should add new todo item when todo item is not in todo list', () => { 62 | // Arrange 63 | const newTodo = { id: 'lala1', title: 'Buy Milk', description: 'Remember to buy milk' }; 64 | 65 | const todoList = [ 66 | { id: 'task1', title: 'Buy Milk', description: 'Remember to buy milk' }, 67 | { id: 'task2', title: 'Go to the gym', description: 'Remember to work out' } 68 | ]; 69 | (todoListServiceMock as any).todoList = todoList; 70 | 71 | // Act 72 | component.currentTODO = newTodo; 73 | const form = new NgForm([], []); 74 | 75 | component.save(form); 76 | 77 | // Assert 78 | expect(addTodoServiceStub.save).toHaveBeenCalledWith(form); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, Output } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | 4 | import { TODOItem } from '@app/shared/models/todo-item'; 5 | import { AddTodoService } from './add-todo.service'; 6 | 7 | @Component({ 8 | selector: 'app-add-todo', 9 | templateUrl: './add-todo.component.html', 10 | styleUrls: ['./add-todo.component.css'], 11 | viewProviders: [AddTodoService] 12 | }) 13 | export class AddTodoComponent implements OnInit { 14 | @Input() 15 | public isLoading = false; 16 | 17 | public get currentTODO(): TODOItem { 18 | return this.addTodoService.currentTODO; 19 | } 20 | @Input() 21 | public set currentTODO(todo: TODOItem) { 22 | this.addTodoService.currentTODO = todo; 23 | } 24 | 25 | @Output() 26 | public get todoItemEdit() { 27 | return this.addTodoService.todoItemEdit; 28 | } 29 | 30 | @Output() 31 | public get todoItemCreate() { 32 | return this.addTodoService.todoItemCreate; 33 | } 34 | 35 | constructor(private addTodoService: AddTodoService) {} 36 | 37 | public ngOnInit() {} 38 | 39 | public save(form: NgForm) { 40 | this.addTodoService.save(form); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { SharedModule } from '@app/shared/shared.module'; 6 | import { AddTodoPresentationComponent } from './add-todo-presentation/add-todo-presentation.component'; 7 | import { AddTodoComponent } from './add-todo.component'; 8 | 9 | @NgModule({ 10 | imports: [FormsModule, CommonModule, SharedModule], 11 | declarations: [AddTodoComponent, AddTodoPresentationComponent], 12 | exports: [AddTodoComponent] 13 | }) 14 | export class AddTodoModule {} 15 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.service.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { inject, TestBed } from '@angular/core/testing'; 4 | 5 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 6 | import { provideMagicalMock } from '@app/helpers/spy-helper'; 7 | import { AddTodoService } from './add-todo.service'; 8 | 9 | describe('Service: AddTodo', () => { 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | providers: [AddTodoService, provideMagicalMock(TodoListService)] 13 | }); 14 | }); 15 | 16 | it('should ...', inject([AddTodoService], (service: AddTodoService) => { 17 | expect(service).toBeTruthy(); 18 | })); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/todo-list/add-todo/add-todo.service.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, Injectable } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | 4 | import { TODOItem } from '@app/shared/models/todo-item'; 5 | 6 | @Injectable() 7 | export class AddTodoService { 8 | public get currentTODO(): TODOItem { 9 | return this._currentTODO; 10 | } 11 | public set currentTODO(value: TODOItem) { 12 | this._currentTODO = { ...value }; 13 | } 14 | 15 | public todoItemEdit = new EventEmitter(); 16 | public todoItemCreate = new EventEmitter(); 17 | private _currentTODO: TODOItem = new TODOItem('', ''); 18 | 19 | public save(form: NgForm) { 20 | if (!form.valid) { 21 | // tslint:disable-next-line:no-console 22 | console.log('Invalid form!'); 23 | // TODO: display form errors 24 | return; 25 | } 26 | 27 | const todoToSave = { 28 | ...this.currentTODO 29 | }; 30 | 31 | if (todoToSave.id) { 32 | this.todoItemEdit.next(todoToSave); 33 | form.resetForm(); 34 | } else { 35 | this.todoItemCreate.next(todoToSave); 36 | form.resetForm(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/todo-list/duedate-today-count.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | 3 | import { DuedateTodayCountPipe } from './duedate-today-count.pipe'; 4 | 5 | describe('Pipe: DuedateTodayCounte', () => { 6 | it('create an instance', () => { 7 | const pipe = new DuedateTodayCountPipe(); 8 | expect(pipe).toBeTruthy(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/todo-list/duedate-today-count.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { TODOItem } from '@app/shared/models/todo-item'; 4 | 5 | @Pipe({ 6 | name: 'duedateTodayCount' 7 | }) 8 | export class DuedateTodayCountPipe implements PipeTransform { 9 | public transform(todoItems: TODOItem[], args?: any): any { 10 | return todoItems.filter((todo) => this.isToday(new Date(todo.dueDate))).length; 11 | } 12 | 13 | private isToday(someDate) { 14 | const today = new Date(); 15 | return ( 16 | someDate.getDate() === today.getDate() && 17 | someDate.getMonth() === today.getMonth() && 18 | someDate.getFullYear() === today.getFullYear() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/todo-list/todo-list.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    {{'todo-list' | translate}}
    4 |
    5 | 6 | 7 |
    8 |
    9 | {{'todo-list-section.todos-duedate-today' | translate}}: {{todoList | duedateTodayCount}} 10 |
    11 |
    12 | 13 |
    14 | 15 | 16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 |
      24 | 25 |
    26 |
    27 | -------------------------------------------------------------------------------- /src/app/todo-list/todo-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/app/todo-list/todo-list.component.scss -------------------------------------------------------------------------------- /src/app/todo-list/todo-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { of } from 'rxjs'; 4 | 5 | import { TodoListActions } from '@app/core/todo-list/redux-api/todo-list.actions'; 6 | import { TodoListSelector } from '@app/core/todo-list/redux-api/todo-list.selector'; 7 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 8 | import { TODOItem } from '@app/shared/models/todo-item'; 9 | import { TodoItemListRowComponentMock } from '@app/shared/todo-item-list-row/todo-item-list-row.component.mock'; 10 | import { AddTodoComponentMock } from '@app/todo-list/add-todo/add-todo.component.mock'; 11 | import { TodoListComponent } from '@app/todo-list/todo-list.component'; 12 | 13 | describe('TodoListComponent', () => { 14 | let component: TodoListComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async(() => { 18 | const todo1 = new TODOItem('Buy milk', 'Remember to buy milk'); 19 | todo1.completed = true; 20 | const todoList = [todo1, new TODOItem('Buy flowers', 'Remember to buy flowers')]; 21 | 22 | TestBed.configureTestingModule({ 23 | declarations: [TodoListComponent, TodoItemListRowComponentMock, AddTodoComponentMock], 24 | imports: [], 25 | providers: [ 26 | { 27 | provide: TodoListService, 28 | useValue: { todoList, getTodoForEdit$: () => of(todo1) } 29 | }, 30 | { 31 | provide: TodoListSelector, 32 | useValue: { 33 | getTodoList: () => of([]) 34 | } 35 | }, 36 | { 37 | provide: TodoListActions, 38 | useValue: { 39 | loadTodoList: () => {} 40 | } 41 | } 42 | ] 43 | }) 44 | .overrideTemplate(TodoListComponent, '') 45 | .compileComponents(); 46 | })); 47 | 48 | beforeEach(() => { 49 | fixture = TestBed.createComponent(TodoListComponent); 50 | component = fixture.componentInstance; 51 | fixture.detectChanges(); 52 | }); 53 | 54 | it('should create', () => { 55 | expect(component).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/app/todo-list/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | 3 | import { TodoListService } from '@app/core/todo-list/todo-list.service'; 4 | import { TODOItem } from '@app/shared/models/todo-item'; 5 | 6 | @Component({ 7 | selector: 'app-todo-list', 8 | templateUrl: './todo-list.component.html', 9 | styleUrls: ['./todo-list.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class TodoListComponent implements OnInit { 13 | public todoList$ = this.todoListService.todoList$; 14 | public selectedTodoForEdit$ = this.todoListService.getTodoForEdit$(); 15 | public isLoading$ = this.todoListService.isLoading$; 16 | 17 | constructor(private todoListService: TodoListService) {} 18 | 19 | public ngOnInit(): void {} 20 | 21 | public deleteTodo(id: string) { 22 | this.todoListService.deleteTodo(id); 23 | } 24 | 25 | public setTodoForEdit(todoItem: TODOItem) { 26 | this.todoListService.setTodoItemForEdit(todoItem); 27 | } 28 | 29 | /** 30 | * todoItemEdit 31 | */ 32 | public todoItemEdit(todoItem: TODOItem) { 33 | this.todoListService.editTodo(todoItem); 34 | } 35 | 36 | /** 37 | * todoItemCreate 38 | */ 39 | public todoItemCreate(todoItem: TODOItem) { 40 | this.todoListService.addTodo(todoItem); 41 | } 42 | 43 | public trackByFn(index, item) { 44 | return item.id; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/todo-list/todo-list.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { EffectsModule } from '@ngrx/effects'; 5 | import { StoreModule } from '@ngrx/store'; 6 | 7 | import { TodoListEffects } from '@app/core/todo-list/redux-api/todo-list.effects'; 8 | import { todoListReducers } from '@app/core/todo-list/redux-api/todo-list.reducers'; 9 | import { TodoListSelector } from '@app/core/todo-list/redux-api/todo-list.selector'; 10 | import { SharedModule } from '@app/shared/shared.module'; 11 | import { TodoListComponent } from '@app/todo-list/todo-list.component'; 12 | import { AddTodoModule } from './add-todo/add-todo.module'; 13 | import { DuedateTodayCountPipe } from './duedate-today-count.pipe'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | FormsModule, 18 | CommonModule, 19 | SharedModule, 20 | AddTodoModule, 21 | StoreModule.forFeature('todoList', todoListReducers), 22 | EffectsModule.forFeature([TodoListEffects]) 23 | ], 24 | declarations: [TodoListComponent, DuedateTodayCountPipe], 25 | providers: [TodoListSelector] 26 | }) 27 | export class TodoListModule {} 28 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/app-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment": "local", 3 | "feServerUrl": "http://localhost:8080" 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/environments/dynamic-environment.ts: -------------------------------------------------------------------------------- 1 | declare var window: any; 2 | 3 | export class DynamicEnvironment { 4 | public get environment() { 5 | return window.config.environment; 6 | } 7 | 8 | public get feServerUrl() { 9 | return window.config.feServerUrl; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | declare var window: any; 2 | 3 | export const environment = { 4 | production: true, 5 | get environment() { 6 | return window.config.environment; 7 | }, 8 | 9 | get feServerUrl() { 10 | return window.config.feServerUrl; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | declare var window: any; 2 | 3 | // This file can be replaced during build by using the `fileReplacements` array. 4 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 5 | // The list of file replacements can be found in `angular.json`. 6 | 7 | export const environment = { 8 | production: false, 9 | 10 | get environment() { 11 | return window.config.environment; 12 | }, 13 | 14 | get feServerUrl() { 15 | return window.config.feServerUrl; 16 | } 17 | }; 18 | 19 | /* 20 | * For easier debugging in development mode, you can import the following file 21 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 22 | * 23 | * This import should be commented out in production mode because it will have a negative impact 24 | * on performance if an error is thrown. 25 | */ 26 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 27 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lydemann/Angular-demo-with-best-practices/c7b140fa1d192a450cf6fe8986bbc2f6ba4857d6/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular demo with best practices 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/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'), 20 | reports: ['html', 'lcovonly'], 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 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { environment } from 'environments/environment'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | // tslint:disable-next-line:no-console 14 | .catch((err) => console.log(err)); 15 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-demo-with-best-practices", 3 | "short_name": "angular-demo-with-best-practices", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "/", 8 | "start_url": "/", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "assets/icons/icon-96x96.png", 17 | "sizes": "96x96", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "assets/icons/icon-128x128.png", 22 | "sizes": "128x128", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "assets/icons/icon-144x144.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "assets/icons/icon-152x152.png", 32 | "sizes": "152x152", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "assets/icons/icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "assets/icons/icon-384x384.png", 42 | "sizes": "384x384", 43 | "type": "image/png" 44 | }, 45 | { 46 | "src": "assets/icons/icon-512x512.png", 47 | "sizes": "512x512", 48 | "type": "image/png" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.component.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 | 15 |

    16 | {{message}} 17 |

    18 |
    19 |
    20 | -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.component.mock.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { SpinnerComponent } from '@shared-lib/spinner/spinner.component'; 3 | 4 | @Component({ 5 | selector: 'app-spinner', 6 | template: '' 7 | }) 8 | export class SpinnerComponentMock implements OnInit, SpinnerComponent { 9 | @Input() message = ''; 10 | 11 | constructor() {} 12 | 13 | ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | @import "~styles/spinner"; -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-unused-variable */ 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | 4 | import { SpinnerComponent } from '@shared-lib/spinner/spinner.component'; 5 | 6 | describe('SpinnerComponent', () => { 7 | let component: SpinnerComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [SpinnerComponent] 13 | }).compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SpinnerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-spinner', 5 | templateUrl: './spinner.component.html', 6 | styleUrls: ['./spinner.component.scss'] 7 | }) 8 | export class SpinnerComponent implements OnInit { 9 | @Input() public message = ''; 10 | 11 | constructor() {} 12 | 13 | public ngOnInit() {} 14 | } 15 | -------------------------------------------------------------------------------- /src/shared-lib/spinner/spinner.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { SpinnerComponent } from '@shared-lib/spinner/spinner.component'; 5 | 6 | @NgModule({ 7 | imports: [CommonModule], 8 | declarations: [SpinnerComponent], 9 | exports: [SpinnerComponent] 10 | }) 11 | export class SpinnerModule {} 12 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | 4 | /* Bootstrap */ 5 | @import '~bootstrap/scss/bootstrap'; 6 | 7 | @import "styles/positioning"; 8 | @import "styles/spinner"; 9 | -------------------------------------------------------------------------------- /src/styles/positioning.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Helpers for setting position using padding and margin 3 | */ 4 | 5 | .p-0 { 6 | padding: 0px; 7 | } 8 | 9 | .p-3 { 10 | padding: 3px; 11 | } 12 | 13 | .p-5 { 14 | padding: 5px; 15 | } 16 | 17 | .p-10 { 18 | padding: 10px; 19 | } 20 | 21 | .p-15 { 22 | padding: 15px; 23 | } 24 | 25 | .p-20 { 26 | padding: 20px; 27 | } 28 | 29 | .p-25 { 30 | padding: 25px; 31 | } 32 | 33 | .p-30 { 34 | padding: 30px; 35 | } 36 | 37 | .p-35 { 38 | padding: 35px; 39 | } 40 | 41 | .p-40 { 42 | padding: 40px; 43 | } 44 | 45 | .p-45 { 46 | padding: 45px; 47 | } 48 | 49 | .p-50 { 50 | padding: 50px; 51 | } 52 | 53 | .pt-0 { 54 | padding-top: 0px; 55 | } 56 | 57 | .pt-3 { 58 | padding-top: 3px; 59 | } 60 | 61 | .pt-5 { 62 | padding-top: 5px; 63 | } 64 | 65 | .pt-10 { 66 | padding-top: 10px; 67 | } 68 | 69 | .pt-15 { 70 | padding-top: 15px; 71 | } 72 | 73 | .pt-20 { 74 | padding-top: 20px; 75 | } 76 | 77 | .pt-25 { 78 | padding-top: 25px; 79 | } 80 | 81 | .pt-30 { 82 | padding-top: 30px; 83 | } 84 | 85 | .pt-35 { 86 | padding-top: 35px; 87 | } 88 | 89 | .pt-40 { 90 | padding-top: 40px; 91 | } 92 | 93 | .pt-45 { 94 | padding-top: 45px; 95 | } 96 | 97 | .pt-50 { 98 | padding-top: 50px; 99 | } 100 | 101 | .pr-0 { 102 | padding-right: 0px; 103 | } 104 | 105 | .pr-3 { 106 | padding-right: 3px; 107 | } 108 | 109 | .pr-5 { 110 | padding-right: 5px; 111 | } 112 | 113 | .pr-10 { 114 | padding-right: 10px; 115 | } 116 | 117 | .pr-15 { 118 | padding-right: 15px; 119 | } 120 | 121 | .pr-20 { 122 | padding-right: 20px; 123 | } 124 | 125 | .pr-25 { 126 | padding-right: 25px; 127 | } 128 | 129 | .pr-30 { 130 | padding-right: 30px; 131 | } 132 | 133 | .pr-35 { 134 | padding-right: 35px; 135 | } 136 | 137 | .pr-40 { 138 | padding-right: 40px; 139 | } 140 | 141 | .pr-45 { 142 | padding-right: 45px; 143 | } 144 | 145 | .pr-50 { 146 | padding-right: 50px; 147 | } 148 | 149 | .pb-0 { 150 | padding-bottom: 0px; 151 | } 152 | 153 | .pb-3 { 154 | padding-bottom: 3px; 155 | } 156 | 157 | .pb-5 { 158 | padding-bottom: 5px; 159 | } 160 | 161 | .pb-10 { 162 | padding-bottom: 10px; 163 | } 164 | 165 | .pb-15 { 166 | padding-bottom: 15px; 167 | } 168 | 169 | .pb-20 { 170 | padding-bottom: 20px; 171 | } 172 | 173 | .pb-25 { 174 | padding-bottom: 25px; 175 | } 176 | 177 | .pb-30 { 178 | padding-bottom: 30px; 179 | } 180 | 181 | .pb-35 { 182 | padding-bottom: 35px; 183 | } 184 | 185 | .pb-40 { 186 | padding-bottom: 40px; 187 | } 188 | 189 | .pb-45 { 190 | padding-bottom: 45px; 191 | } 192 | 193 | .pb-50 { 194 | padding-bottom: 50px; 195 | } 196 | 197 | .pl-0 { 198 | padding-left: 0px; 199 | } 200 | 201 | .pl-3 { 202 | padding-left: 3px; 203 | } 204 | 205 | .pl-5 { 206 | padding-left: 5px; 207 | } 208 | 209 | .pl-10 { 210 | padding-left: 10px; 211 | } 212 | 213 | .pl-15 { 214 | padding-left: 15px; 215 | } 216 | 217 | .pl-20 { 218 | padding-left: 20px; 219 | } 220 | 221 | .pl-25 { 222 | padding-left: 25px; 223 | } 224 | 225 | .pl-30 { 226 | padding-left: 30px; 227 | } 228 | 229 | .pl-35 { 230 | padding-left: 35px; 231 | } 232 | 233 | .pl-40 { 234 | padding-left: 40px; 235 | } 236 | 237 | .pl-45 { 238 | padding-left: 45px; 239 | } 240 | 241 | .pl-50 { 242 | padding-left: 50px; 243 | } 244 | 245 | .m-0 { 246 | margin: 0px; 247 | } 248 | 249 | .m-3 { 250 | margin: 3px; 251 | } 252 | 253 | .m-5 { 254 | margin: 5px; 255 | } 256 | 257 | .m-10 { 258 | margin: 10px; 259 | } 260 | 261 | .m-15 { 262 | margin: 15px; 263 | } 264 | 265 | .m-20 { 266 | margin: 20px; 267 | } 268 | 269 | .m-25 { 270 | margin: 25px; 271 | } 272 | 273 | .m-30 { 274 | margin: 30px; 275 | } 276 | 277 | .m-35 { 278 | margin: 35px; 279 | } 280 | 281 | .m-40 { 282 | margin: 40px; 283 | } 284 | 285 | .m-45 { 286 | margin: 45px; 287 | } 288 | 289 | .m-50 { 290 | margin: 50px; 291 | } 292 | 293 | .mt-0 { 294 | margin-top: 0px; 295 | } 296 | 297 | .mt-3 { 298 | margin-top: 3px; 299 | } 300 | 301 | .mt-5 { 302 | margin-top: 5px; 303 | } 304 | 305 | .mt-10 { 306 | margin-top: 10px; 307 | } 308 | 309 | .mt-15 { 310 | margin-top: 15px; 311 | } 312 | 313 | .mt-20 { 314 | margin-top: 20px; 315 | } 316 | 317 | .mt-25 { 318 | margin-top: 25px; 319 | } 320 | 321 | .mt-30 { 322 | margin-top: 30px; 323 | } 324 | 325 | .mt-35 { 326 | margin-top: 35px; 327 | } 328 | 329 | .mt-40 { 330 | margin-top: 40px; 331 | } 332 | 333 | .mt-45 { 334 | margin-top: 45px; 335 | } 336 | 337 | .mt-50 { 338 | margin-top: 50px; 339 | } 340 | 341 | .mr-0 { 342 | margin-right: 0px; 343 | } 344 | 345 | .mr-3 { 346 | margin-right: 3px; 347 | } 348 | 349 | .mr-5 { 350 | margin-right: 5px; 351 | } 352 | 353 | .mr-10 { 354 | margin-right: 10px; 355 | } 356 | 357 | .mr-15 { 358 | margin-right: 15px; 359 | } 360 | 361 | .mr-20 { 362 | margin-right: 20px; 363 | } 364 | 365 | .mr-25 { 366 | margin-right: 25px; 367 | } 368 | 369 | .mr-30 { 370 | margin-right: 30px; 371 | } 372 | 373 | .mr-35 { 374 | margin-right: 35px; 375 | } 376 | 377 | .mr-40 { 378 | margin-right: 40px; 379 | } 380 | 381 | .mr-45 { 382 | margin-right: 45px; 383 | } 384 | 385 | .mr-50 { 386 | margin-right: 50px; 387 | } 388 | 389 | .mb-0 { 390 | margin-bottom: 0px; 391 | } 392 | 393 | .mb-3 { 394 | margin-bottom: 3px; 395 | } 396 | 397 | .mb-5 { 398 | margin-bottom: 5px; 399 | } 400 | 401 | .mb-10 { 402 | margin-bottom: 10px; 403 | } 404 | 405 | .mb-15 { 406 | margin-bottom: 15px; 407 | } 408 | 409 | .mb-20 { 410 | margin-bottom: 20px; 411 | } 412 | 413 | .mb-25 { 414 | margin-bottom: 25px; 415 | } 416 | 417 | .mb-30 { 418 | margin-bottom: 30px; 419 | } 420 | 421 | .mb-35 { 422 | margin-bottom: 35px; 423 | } 424 | 425 | .mb-40 { 426 | margin-bottom: 40px; 427 | } 428 | 429 | .mb-45 { 430 | margin-bottom: 45px; 431 | } 432 | 433 | .mb-50 { 434 | margin-bottom: 50px; 435 | } 436 | 437 | .ml-0 { 438 | margin-left: 0px; 439 | } 440 | 441 | .ml-3 { 442 | margin-left: 3px; 443 | } 444 | 445 | .ml-5 { 446 | margin-left: 5px; 447 | } 448 | 449 | .ml-10 { 450 | margin-left: 10px; 451 | } 452 | 453 | .ml-15 { 454 | margin-left: 15px; 455 | } 456 | 457 | .ml-20 { 458 | margin-left: 20px; 459 | } 460 | 461 | .ml-25 { 462 | margin-left: 25px; 463 | } 464 | 465 | .ml-30 { 466 | margin-left: 30px; 467 | } 468 | 469 | .ml-35 { 470 | margin-left: 35px; 471 | } 472 | 473 | .ml-40 { 474 | margin-left: 40px; 475 | } 476 | 477 | .ml-45 { 478 | margin-left: 45px; 479 | } 480 | 481 | .ml-50 { 482 | margin-left: 50px; 483 | } 484 | 485 | .mh-a { 486 | margin: 0 auto; 487 | } 488 | 489 | .pull-right { 490 | margin-left: auto; 491 | } 492 | -------------------------------------------------------------------------------- /src/styles/spinner.scss: -------------------------------------------------------------------------------- 1 | #loader { 2 | bottom: 0; 3 | height: 175px; 4 | left: 0; 5 | margin: auto; 6 | position: absolute; 7 | right: 0; 8 | top: 0; 9 | width: 175px; 10 | } 11 | 12 | #loader { 13 | bottom: 0; 14 | height: 175px; 15 | left: 0; 16 | margin: auto; 17 | position: absolute; 18 | right: 0; 19 | top: 0; 20 | width: 175px; 21 | } 22 | 23 | #loader .dot { 24 | bottom: 0; 25 | height: 100%; 26 | left: 0; 27 | margin: auto; 28 | position: absolute; 29 | right: 0; 30 | top: 0; 31 | width: 87.5px; 32 | } 33 | 34 | #loader .dot::before { 35 | border-radius: 100%; 36 | content: ""; 37 | height: 87.5px; 38 | left: 0; 39 | position: absolute; 40 | right: 0; 41 | top: 0; 42 | transform: scale(0); 43 | width: 87.5px; 44 | } 45 | 46 | #loader .dot:nth-child(7n+1) { 47 | transform: rotate(45deg); 48 | } 49 | 50 | #loader .dot:nth-child(7n+1)::before { 51 | animation: 0.8s linear 0.1s normal none infinite running load; 52 | background: #00ff80 none repeat scroll 0 0; 53 | } 54 | 55 | #loader .dot:nth-child(7n+2) { 56 | transform: rotate(90deg); 57 | } 58 | 59 | #loader .dot:nth-child(7n+2)::before { 60 | animation: 0.8s linear 0.2s normal none infinite running load; 61 | background: #00ffea none repeat scroll 0 0; 62 | } 63 | 64 | #loader .dot:nth-child(7n+3) { 65 | transform: rotate(135deg); 66 | } 67 | 68 | #loader .dot:nth-child(7n+3)::before { 69 | animation: 0.8s linear 0.3s normal none infinite running load; 70 | background: #00aaff none repeat scroll 0 0; 71 | } 72 | 73 | #loader .dot:nth-child(7n+4) { 74 | transform: rotate(180deg); 75 | } 76 | 77 | #loader .dot:nth-child(7n+4)::before { 78 | animation: 0.8s linear 0.4s normal none infinite running load; 79 | background: #0040ff none repeat scroll 0 0; 80 | } 81 | 82 | #loader .dot:nth-child(7n+5) { 83 | transform: rotate(225deg); 84 | } 85 | 86 | #loader .dot:nth-child(7n+5)::before { 87 | animation: 0.8s linear 0.5s normal none infinite running load; 88 | background: #2a00ff none repeat scroll 0 0; 89 | } 90 | 91 | #loader .dot:nth-child(7n+6) { 92 | transform: rotate(270deg); 93 | } 94 | 95 | #loader .dot:nth-child(7n+6)::before { 96 | animation: 0.8s linear 0.6s normal none infinite running load; 97 | background: #9500ff none repeat scroll 0 0; 98 | } 99 | 100 | #loader .dot:nth-child(7n+7) { 101 | transform: rotate(315deg); 102 | } 103 | 104 | #loader .dot:nth-child(7n+7)::before { 105 | animation: 0.8s linear 0.7s normal none infinite running load; 106 | background: magenta none repeat scroll 0 0; 107 | } 108 | 109 | #loader .dot:nth-child(7n+8) { 110 | transform: rotate(360deg); 111 | } 112 | 113 | #loader .dot:nth-child(7n+8)::before { 114 | animation: 0.8s linear 0.8s normal none infinite running load; 115 | background: #ff0095 none repeat scroll 0 0; 116 | } 117 | 118 | #loader .loading { 119 | background-position: 50% 50%; 120 | background-repeat: no-repeat; 121 | bottom: -40px; 122 | height: 20px; 123 | left: 0; 124 | position: absolute; 125 | right: 0; 126 | width: 180px; 127 | } 128 | 129 | @keyframes load { 130 | 100% { 131 | opacity: 0; 132 | transform: scale(1); 133 | } 134 | } 135 | 136 | @keyframes load { 137 | 100% { 138 | opacity: 0; 139 | transform: scale(1); 140 | } 141 | } 142 | 143 | .spinner-message { 144 | text-align: center; 145 | } 146 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | import 'zone.js/dist/zone-testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 14 | // Then we find all the tests. 15 | const context = require.context('./', true, /\.spec\.ts$/); 16 | // And load the modules. 17 | context.keys().map(context); 18 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "exclude": [ 11 | "src/test.ts", 12 | "**/*.mock.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "src/test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.mock.ts", 14 | "src/**/spy-helper.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "src", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "typeRoots": [ 16 | "node_modules/@types" 17 | ], 18 | "lib": [ 19 | "es2018", 20 | "dom" 21 | ], 22 | "paths": { 23 | "@app/*": [ 24 | "app/*" 25 | ], 26 | "@shared-lib/*": [ 27 | "shared-lib/*" 28 | ] 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended", 4 | "tslint-eslint-rules", 5 | "tslint-config-prettier", 6 | "tslint-jasmine-rules" 7 | ], 8 | "rulesDirectory": [ 9 | "node_modules/codelyzer" 10 | ], 11 | "rules": { 12 | "deprecation": { 13 | "severity": "warn" 14 | }, 15 | "member-access": [true, "check-accessor"], 16 | "member-ordering": [ 17 | true, 18 | { 19 | "order": "instance-sandwich" 20 | } 21 | ], 22 | "no-focused-tests": true, 23 | "no-inferrable-types": [false, "ignore-params"], 24 | "component-selector": [true, "element", "app", "kebab-case"], 25 | "array-bracket-spacing": [true, "never"], 26 | "object-curly-spacing": [true, "always"], 27 | "max-classes-per-file": false, 28 | "component-class-suffix": true, 29 | "directive-class-suffix": true, 30 | "directive-selector": [true, "attribute", "app", "camelCase"], 31 | "import-destructuring-spacing": true, 32 | "no-forward-ref": true, 33 | "no-input-rename": true, 34 | "no-output-on-prefix": true, 35 | "no-output-rename": true, 36 | "use-life-cycle-interface": true, 37 | "use-pipe-transform-interface": true, 38 | "no-non-null-assertion": true, 39 | "no-switch-case-fall-through": true, 40 | "no-empty": false, 41 | "jsdoc-format": false, 42 | "one-line": [true, "check-open-brace"], 43 | "align": false, 44 | "import-blacklist": [true, "rxjs/Rx"], 45 | "interface-name": false, 46 | "no-bitwise": true, 47 | "no-conditional-assignment": true, 48 | "no-console": true, 49 | "no-empty-interface": false, 50 | "no-string-literal": false, 51 | "object-literal-sort-keys": false, 52 | "only-arrow-functions": [true, "allow-named-functions"], 53 | "ordered-imports": [ 54 | true, 55 | { 56 | "grouped-imports": true, 57 | "groups": [{ 58 | "name": "app", 59 | "match": "^@app", 60 | "order": 20 61 | }, 62 | { 63 | "name": "shared-lib", 64 | "match": "^@shared-lib", 65 | "order": 20 66 | }, 67 | { 68 | "name": "relative_paths", 69 | "match": "^[.][.]?", 70 | "order": 20 71 | }, 72 | { 73 | "name": "scoped_paths", 74 | "match": "^@", 75 | "order": 10 76 | }, 77 | { 78 | "name": "absolute_paths", 79 | "match": "^[a-zA-Z]", 80 | "order": 10 81 | }, 82 | { 83 | "match": null, 84 | "order": 10 85 | } 86 | ] 87 | } 88 | ], 89 | "prefer-for-of": true, 90 | "prefer-object-spread": true, 91 | "quotemark": [true, "single", "avoid-escape"], 92 | "trailing-comma": [ 93 | false, 94 | { 95 | "multiline": "always", 96 | "singleline": "never" 97 | } 98 | ], 99 | "variable-name": [ 100 | true, 101 | "allow-leading-underscore", 102 | "allow-pascal-case", 103 | "ban-keywords", 104 | "check-format" 105 | ] 106 | } 107 | } 108 | --------------------------------------------------------------------------------