├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── README.md ├── angular.json ├── apps ├── .gitkeep ├── api │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── .gitkeep │ │ │ ├── app.controller.spec.ts │ │ │ ├── app.controller.ts │ │ │ ├── app.module.ts │ │ │ ├── app.service.spec.ts │ │ │ └── app.service.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── main.ts │ │ └── widgets │ │ │ ├── dto │ │ │ ├── create-widget.dto.ts │ │ │ └── update-widget.dto.ts │ │ │ ├── entities │ │ │ └── widget.entity.ts │ │ │ ├── widgets.controller.spec.ts │ │ │ ├── widgets.controller.ts │ │ │ ├── widgets.module.ts │ │ │ ├── widgets.service.spec.ts │ │ │ └── widgets.service.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── client-e2e │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json ├── client │ ├── .browserslistrc │ ├── jest.config.js │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ └── app.module.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── logo.png │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── dashboard-e2e │ ├── cypress.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ ├── tsconfig.json │ └── tslint.json └── dashboard │ ├── .browserslistrc │ ├── jest.config.js │ ├── proxy.conf.json │ ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ └── home.component.ts │ │ ├── routing.module.ts │ │ └── widgets │ │ │ ├── widget-details │ │ │ ├── widget-details.component.html │ │ │ ├── widget-details.component.scss │ │ │ ├── widget-details.component.spec.ts │ │ │ └── widget-details.component.ts │ │ │ ├── widgets-list │ │ │ ├── widgets-list.component.html │ │ │ ├── widgets-list.component.scss │ │ │ ├── widgets-list.component.spec.ts │ │ │ └── widgets-list.component.ts │ │ │ ├── widgets.component.html │ │ │ ├── widgets.component.scss │ │ │ ├── widgets.component.spec.ts │ │ │ └── widgets.component.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── logo.png │ │ └── screenshots │ │ │ ├── api.png │ │ │ └── app.png │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.editor.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── decorate-angular-cli.js ├── jest.config.js ├── jest.preset.js ├── libs ├── .gitkeep ├── api-interfaces │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ └── api-interfaces.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── core-data │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── core-data.module.ts │ │ │ └── services │ │ │ │ └── widgets │ │ │ │ ├── widgets.service.spec.ts │ │ │ │ └── widgets.service.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── core-state │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── core-state.module.ts │ │ │ ├── index.ts │ │ │ └── widgets │ │ │ │ ├── widgets.actions.ts │ │ │ │ ├── widgets.effects.spec.ts │ │ │ │ ├── widgets.effects.ts │ │ │ │ ├── widgets.facade.spec.ts │ │ │ │ ├── widgets.facade.ts │ │ │ │ ├── widgets.models.ts │ │ │ │ ├── widgets.reducer.spec.ts │ │ │ │ ├── widgets.reducer.ts │ │ │ │ ├── widgets.selectors.spec.ts │ │ │ │ └── widgets.selectors.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── material │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ └── material.module.ts │ │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json └── ui-toolbar │ ├── README.md │ ├── jest.config.js │ ├── src │ ├── index.ts │ ├── lib │ │ ├── toolbar │ │ │ └── toolbar │ │ │ │ ├── toolbar.component.html │ │ │ │ ├── toolbar.component.scss │ │ │ │ ├── toolbar.component.spec.ts │ │ │ │ └── toolbar.component.ts │ │ └── ui-toolbar.module.ts │ └── test-setup.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── nx.json ├── package.json ├── server └── db.json ├── slides.pdf ├── tools ├── schematics │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "angular.ng-template", 5 | "ms-vscode.vscode-typescript-tslint-plugin", 6 | "esbenp.prettier-vscode", 7 | "firsttris.vscode-jest-runner" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Production Course 2 | 3 | ![Angular Production Course App](apps/dashboard/src/assets/screenshots/app.png) 4 | 5 | This is the sample project for the Angular Production course for Frontend Masters. 6 | 7 | The sample project includes an Angular web application and a mock RESTful API within an Nx workspace by NRWL. The Angular application uses state and data libs to manage state and handle server communication. The state lib is built around NgRx and the application is entirely reactive. 8 | 9 | ## Prerequisites 10 | - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 11 | - Node.js and NPM – we recommend using [NVM (Linux/Mac)](https://github.com/creationix/nvm) or [NVM-Windows (Windows)](https://github.com/coreybutler/nvm-windows) 12 | - Install Angular CLI via `npm i -g @angular/cli` 13 | 14 | ## Web: Getting Started 15 | 16 | ``` 17 | git clone https://github.com/onehungrymind/fem-production-angular.git 18 | cd fem-production-angular 19 | yarn 20 | npm run serve:all 21 | ``` 22 | 23 | The `serve:all` command is a convenience methods that runs the `serve:api` and `serve:web` commands concurrently. You can run each command separately if you need to. 24 | 25 | ``` 26 | "serve:api": "nx run api:serve", 27 | "serve:web": "ng serve --open", 28 | "serve:all": "concurrently \"npm run serve:api\" \"npm run serve:web\"" 29 | ``` 30 | 31 | The web application will open to [http://localhost:4200](http://localhost:4200) in your browser. 32 | 33 | ![Angular Production Course Api](apps/dashboard/src/assets/screenshots/api.png) 34 | 35 | You can see the API by navigating to [http://localhost:3333/api/](http://localhost:3333/api/) in your browser. 36 | 37 | > Note: the above terminal commands are for Mac. Remember to substitute the appropriate commands for your OS. 38 | 39 | ## Web: Running E2E 40 | 41 | Because Cypress ships with Nx, E2E tests can be run with the command below. 42 | 43 | ``` 44 | npm run e2e 45 | ``` 46 | 47 | You can have Cypress watch and restart tests on test file changes with this command. 48 | 49 | ``` 50 | nx run dashboard-e2e:e2e --watch 51 | ``` 52 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "projects": { 4 | "dashboard": { 5 | "projectType": "application", 6 | "schematics": { 7 | "@schematics/angular:component": { 8 | "style": "scss" 9 | } 10 | }, 11 | "root": "apps/dashboard", 12 | "sourceRoot": "apps/dashboard/src", 13 | "prefix": "fem", 14 | "architect": { 15 | "build": { 16 | "builder": "@angular-devkit/build-angular:browser", 17 | "options": { 18 | "outputPath": "dist/apps/dashboard", 19 | "index": "apps/dashboard/src/index.html", 20 | "main": "apps/dashboard/src/main.ts", 21 | "polyfills": "apps/dashboard/src/polyfills.ts", 22 | "tsConfig": "apps/dashboard/tsconfig.app.json", 23 | "aot": true, 24 | "assets": [ 25 | "apps/dashboard/src/favicon.ico", 26 | "apps/dashboard/src/assets" 27 | ], 28 | "styles": [ 29 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 30 | "apps/dashboard/src/styles.scss" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "apps/dashboard/src/environments/environment.ts", 39 | "with": "apps/dashboard/src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "extractCss": true, 46 | "namedChunks": false, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "6kb", 59 | "maximumError": "10kb" 60 | } 61 | ] 62 | } 63 | } 64 | }, 65 | "serve": { 66 | "builder": "@angular-devkit/build-angular:dev-server", 67 | "options": { 68 | "browserTarget": "dashboard:build", 69 | "proxyConfig": "apps/dashboard/proxy.conf.json" 70 | }, 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "dashboard:build:production" 74 | } 75 | } 76 | }, 77 | "extract-i18n": { 78 | "builder": "@angular-devkit/build-angular:extract-i18n", 79 | "options": { 80 | "browserTarget": "dashboard:build" 81 | } 82 | }, 83 | "lint": { 84 | "builder": "@angular-devkit/build-angular:tslint", 85 | "options": { 86 | "tsConfig": [ 87 | "apps/dashboard/tsconfig.app.json", 88 | "apps/dashboard/tsconfig.spec.json", 89 | "apps/dashboard/tsconfig.editor.json" 90 | ], 91 | "exclude": ["**/node_modules/**", "!apps/dashboard/**/*"] 92 | } 93 | }, 94 | "test": { 95 | "builder": "@nrwl/jest:jest", 96 | "options": { 97 | "jestConfig": "apps/dashboard/jest.config.js", 98 | "passWithNoTests": true 99 | } 100 | } 101 | } 102 | }, 103 | "dashboard-e2e": { 104 | "root": "apps/dashboard-e2e", 105 | "sourceRoot": "apps/dashboard-e2e/src", 106 | "projectType": "application", 107 | "architect": { 108 | "e2e": { 109 | "builder": "@nrwl/cypress:cypress", 110 | "options": { 111 | "cypressConfig": "apps/dashboard-e2e/cypress.json", 112 | "tsConfig": "apps/dashboard-e2e/tsconfig.e2e.json", 113 | "devServerTarget": "dashboard:serve" 114 | }, 115 | "configurations": { 116 | "production": { 117 | "devServerTarget": "dashboard:serve:production" 118 | } 119 | } 120 | }, 121 | "lint": { 122 | "builder": "@angular-devkit/build-angular:tslint", 123 | "options": { 124 | "tsConfig": ["apps/dashboard-e2e/tsconfig.e2e.json"], 125 | "exclude": ["**/node_modules/**", "!apps/dashboard-e2e/**/*"] 126 | } 127 | } 128 | } 129 | }, 130 | "api": { 131 | "root": "apps/api", 132 | "sourceRoot": "apps/api/src", 133 | "projectType": "application", 134 | "prefix": "api", 135 | "schematics": {}, 136 | "architect": { 137 | "build": { 138 | "builder": "@nrwl/node:build", 139 | "options": { 140 | "outputPath": "dist/apps/api", 141 | "main": "apps/api/src/main.ts", 142 | "tsConfig": "apps/api/tsconfig.app.json", 143 | "assets": ["apps/api/src/assets"] 144 | }, 145 | "configurations": { 146 | "production": { 147 | "optimization": true, 148 | "extractLicenses": true, 149 | "inspect": false, 150 | "fileReplacements": [ 151 | { 152 | "replace": "apps/api/src/environments/environment.ts", 153 | "with": "apps/api/src/environments/environment.prod.ts" 154 | } 155 | ] 156 | } 157 | } 158 | }, 159 | "serve": { 160 | "builder": "@nrwl/node:execute", 161 | "options": { 162 | "buildTarget": "api:build" 163 | } 164 | }, 165 | "lint": { 166 | "builder": "@angular-devkit/build-angular:tslint", 167 | "options": { 168 | "tsConfig": [ 169 | "apps/api/tsconfig.app.json", 170 | "apps/api/tsconfig.spec.json" 171 | ], 172 | "exclude": ["**/node_modules/**", "!apps/api/**/*"] 173 | } 174 | }, 175 | "test": { 176 | "builder": "@nrwl/jest:jest", 177 | "options": { 178 | "jestConfig": "apps/api/jest.config.js", 179 | "passWithNoTests": true 180 | } 181 | } 182 | } 183 | }, 184 | "api-interfaces": { 185 | "root": "libs/api-interfaces", 186 | "sourceRoot": "libs/api-interfaces/src", 187 | "projectType": "library", 188 | "schematics": {}, 189 | "architect": { 190 | "lint": { 191 | "builder": "@angular-devkit/build-angular:tslint", 192 | "options": { 193 | "tsConfig": [ 194 | "libs/api-interfaces/tsconfig.lib.json", 195 | "libs/api-interfaces/tsconfig.spec.json" 196 | ], 197 | "exclude": ["**/node_modules/**", "!libs/api-interfaces/**/*"] 198 | } 199 | }, 200 | "test": { 201 | "builder": "@nrwl/jest:jest", 202 | "options": { 203 | "jestConfig": "libs/api-interfaces/jest.config.js", 204 | "passWithNoTests": true 205 | } 206 | } 207 | } 208 | }, 209 | "core-data": { 210 | "projectType": "library", 211 | "root": "libs/core-data", 212 | "sourceRoot": "libs/core-data/src", 213 | "prefix": "fem", 214 | "architect": { 215 | "lint": { 216 | "builder": "@angular-devkit/build-angular:tslint", 217 | "options": { 218 | "tsConfig": [ 219 | "libs/core-data/tsconfig.lib.json", 220 | "libs/core-data/tsconfig.spec.json" 221 | ], 222 | "exclude": ["**/node_modules/**", "!libs/core-data/**/*"] 223 | } 224 | }, 225 | "test": { 226 | "builder": "@nrwl/jest:jest", 227 | "options": { 228 | "jestConfig": "libs/core-data/jest.config.js", 229 | "passWithNoTests": true 230 | } 231 | } 232 | }, 233 | "schematics": { 234 | "@schematics/angular:component": { 235 | "style": "scss" 236 | } 237 | } 238 | }, 239 | "core-state": { 240 | "projectType": "library", 241 | "root": "libs/core-state", 242 | "sourceRoot": "libs/core-state/src", 243 | "prefix": "fem", 244 | "architect": { 245 | "lint": { 246 | "builder": "@angular-devkit/build-angular:tslint", 247 | "options": { 248 | "tsConfig": [ 249 | "libs/core-state/tsconfig.lib.json", 250 | "libs/core-state/tsconfig.spec.json" 251 | ], 252 | "exclude": ["**/node_modules/**", "!libs/core-state/**/*"] 253 | } 254 | }, 255 | "test": { 256 | "builder": "@nrwl/jest:jest", 257 | "options": { 258 | "jestConfig": "libs/core-state/jest.config.js", 259 | "passWithNoTests": true 260 | } 261 | } 262 | }, 263 | "schematics": { 264 | "@schematics/angular:component": { 265 | "style": "scss" 266 | } 267 | } 268 | }, 269 | "material": { 270 | "projectType": "library", 271 | "root": "libs/material", 272 | "sourceRoot": "libs/material/src", 273 | "prefix": "fem", 274 | "architect": { 275 | "lint": { 276 | "builder": "@angular-devkit/build-angular:tslint", 277 | "options": { 278 | "tsConfig": [ 279 | "libs/material/tsconfig.lib.json", 280 | "libs/material/tsconfig.spec.json" 281 | ], 282 | "exclude": ["**/node_modules/**", "!libs/material/**/*"] 283 | } 284 | }, 285 | "test": { 286 | "builder": "@nrwl/jest:jest", 287 | "options": { 288 | "jestConfig": "libs/material/jest.config.js", 289 | "passWithNoTests": true 290 | } 291 | } 292 | }, 293 | "schematics": { 294 | "@schematics/angular:component": { 295 | "style": "scss" 296 | } 297 | } 298 | }, 299 | "client": { 300 | "projectType": "application", 301 | "schematics": { 302 | "@schematics/angular:component": { 303 | "style": "scss" 304 | } 305 | }, 306 | "root": "apps/client", 307 | "sourceRoot": "apps/client/src", 308 | "prefix": "fem", 309 | "architect": { 310 | "build": { 311 | "builder": "@angular-devkit/build-angular:browser", 312 | "options": { 313 | "outputPath": "dist/apps/client", 314 | "index": "apps/client/src/index.html", 315 | "main": "apps/client/src/main.ts", 316 | "polyfills": "apps/client/src/polyfills.ts", 317 | "tsConfig": "apps/client/tsconfig.app.json", 318 | "aot": true, 319 | "assets": ["apps/client/src/favicon.ico", "apps/client/src/assets"], 320 | "styles": ["apps/client/src/styles.scss"], 321 | "scripts": [] 322 | }, 323 | "configurations": { 324 | "production": { 325 | "fileReplacements": [ 326 | { 327 | "replace": "apps/client/src/environments/environment.ts", 328 | "with": "apps/client/src/environments/environment.prod.ts" 329 | } 330 | ], 331 | "optimization": true, 332 | "outputHashing": "all", 333 | "sourceMap": false, 334 | "extractCss": true, 335 | "namedChunks": false, 336 | "extractLicenses": true, 337 | "vendorChunk": false, 338 | "buildOptimizer": true, 339 | "budgets": [ 340 | { 341 | "type": "initial", 342 | "maximumWarning": "2mb", 343 | "maximumError": "5mb" 344 | }, 345 | { 346 | "type": "anyComponentStyle", 347 | "maximumWarning": "6kb", 348 | "maximumError": "10kb" 349 | } 350 | ] 351 | } 352 | } 353 | }, 354 | "serve": { 355 | "builder": "@angular-devkit/build-angular:dev-server", 356 | "options": { 357 | "browserTarget": "client:build" 358 | }, 359 | "configurations": { 360 | "production": { 361 | "browserTarget": "client:build:production" 362 | } 363 | } 364 | }, 365 | "extract-i18n": { 366 | "builder": "@angular-devkit/build-angular:extract-i18n", 367 | "options": { 368 | "browserTarget": "client:build" 369 | } 370 | }, 371 | "lint": { 372 | "builder": "@angular-devkit/build-angular:tslint", 373 | "options": { 374 | "tsConfig": [ 375 | "apps/client/tsconfig.app.json", 376 | "apps/client/tsconfig.spec.json", 377 | "apps/client/tsconfig.editor.json" 378 | ], 379 | "exclude": ["**/node_modules/**", "!apps/client/**/*"] 380 | } 381 | }, 382 | "test": { 383 | "builder": "@nrwl/jest:jest", 384 | "options": { 385 | "jestConfig": "apps/client/jest.config.js", 386 | "passWithNoTests": true 387 | } 388 | } 389 | } 390 | }, 391 | "client-e2e": { 392 | "root": "apps/client-e2e", 393 | "sourceRoot": "apps/client-e2e/src", 394 | "projectType": "application", 395 | "architect": { 396 | "e2e": { 397 | "builder": "@nrwl/cypress:cypress", 398 | "options": { 399 | "cypressConfig": "apps/client-e2e/cypress.json", 400 | "tsConfig": "apps/client-e2e/tsconfig.e2e.json", 401 | "devServerTarget": "client:serve" 402 | }, 403 | "configurations": { 404 | "production": { 405 | "devServerTarget": "client:serve:production" 406 | } 407 | } 408 | }, 409 | "lint": { 410 | "builder": "@angular-devkit/build-angular:tslint", 411 | "options": { 412 | "tsConfig": ["apps/client-e2e/tsconfig.e2e.json"], 413 | "exclude": ["**/node_modules/**", "!apps/client-e2e/**/*"] 414 | } 415 | } 416 | } 417 | }, 418 | "ui-toolbar": { 419 | "projectType": "library", 420 | "root": "libs/ui-toolbar", 421 | "sourceRoot": "libs/ui-toolbar/src", 422 | "prefix": "fem", 423 | "architect": { 424 | "lint": { 425 | "builder": "@angular-devkit/build-angular:tslint", 426 | "options": { 427 | "tsConfig": [ 428 | "libs/ui-toolbar/tsconfig.lib.json", 429 | "libs/ui-toolbar/tsconfig.spec.json" 430 | ], 431 | "exclude": ["**/node_modules/**", "!libs/ui-toolbar/**/*"] 432 | } 433 | }, 434 | "test": { 435 | "builder": "@nrwl/jest:jest", 436 | "options": { 437 | "jestConfig": "libs/ui-toolbar/jest.config.js", 438 | "passWithNoTests": true 439 | } 440 | } 441 | }, 442 | "schematics": { 443 | "@schematics/angular:component": { 444 | "style": "scss" 445 | } 446 | } 447 | } 448 | }, 449 | "cli": { 450 | "defaultCollection": "@nrwl/angular" 451 | }, 452 | "schematics": { 453 | "@nrwl/workspace": { 454 | "library": { 455 | "linter": "tslint" 456 | } 457 | }, 458 | "@nrwl/cypress": { 459 | "cypress-project": { 460 | "linter": "tslint" 461 | } 462 | }, 463 | "@nrwl/node": { 464 | "application": { 465 | "linter": "tslint" 466 | }, 467 | "library": { 468 | "linter": "tslint" 469 | } 470 | }, 471 | "@nrwl/nest": { 472 | "application": { 473 | "linter": "tslint" 474 | }, 475 | "library": { 476 | "linter": "tslint" 477 | } 478 | }, 479 | "@nrwl/express": { 480 | "application": { 481 | "linter": "tslint" 482 | }, 483 | "library": { 484 | "linter": "tslint" 485 | } 486 | }, 487 | "@nrwl/angular:application": { 488 | "unitTestRunner": "jest", 489 | "e2eTestRunner": "cypress" 490 | }, 491 | "@nrwl/angular:library": { 492 | "unitTestRunner": "jest" 493 | } 494 | }, 495 | "defaultProject": "dashboard" 496 | } 497 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/.gitkeep -------------------------------------------------------------------------------- /apps/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'api', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]s$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'js', 'html'], 13 | coverageDirectory: '../../coverage/apps/api', 14 | }; 15 | -------------------------------------------------------------------------------- /apps/api/src/app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/api/src/app/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let app: TestingModule; 8 | 9 | beforeAll(async () => { 10 | app = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Welcome to api!"', () => { 18 | const appController = app.get(AppController); 19 | expect(appController.getData()).toEqual({ message: 'Welcome to api!' }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /apps/api/src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | import { Message } from '@fem/api-interfaces'; 4 | 5 | import { AppService } from './app.service'; 6 | 7 | @Controller() 8 | export class AppController { 9 | constructor(private readonly appService: AppService) {} 10 | 11 | @Get('hello') 12 | getData(): Message { 13 | return this.appService.getData(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/api/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WidgetsModule } from '../widgets/widgets.module'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | @Module({ 7 | imports: [WidgetsModule], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /apps/api/src/app/app.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppService', () => { 6 | let service: AppService; 7 | 8 | beforeAll(async () => { 9 | const app = await Test.createTestingModule({ 10 | providers: [AppService], 11 | }).compile(); 12 | 13 | service = app.get(AppService); 14 | }); 15 | 16 | describe('getData', () => { 17 | it('should return "Welcome to api!"', () => { 18 | expect(service.getData()).toEqual({ message: 'Welcome to api!' }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/api/src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Message } from '@fem/api-interfaces'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | getData(): Message { 7 | return { message: 'Welcome to api!' }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/api/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import { Logger } from '@nestjs/common'; 7 | import { NestFactory } from '@nestjs/core'; 8 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 9 | 10 | import { AppModule } from './app/app.module'; 11 | 12 | const configureSwagger = (app) => { 13 | const options = new DocumentBuilder() 14 | .setTitle('Production Angular API') 15 | .setDescription('REST API for the Production Angular course') 16 | .setVersion('1.0') 17 | .build(); 18 | const document = SwaggerModule.createDocument(app, options); 19 | SwaggerModule.setup('api', app, document); 20 | }; 21 | 22 | async function bootstrap() { 23 | const app = await NestFactory.create(AppModule); 24 | const globalPrefix = 'api'; 25 | const port = process.env.PORT || 3333; 26 | 27 | app.setGlobalPrefix(globalPrefix); 28 | app.enableCors(); 29 | 30 | configureSwagger(app); 31 | 32 | await app.listen(port, () => { 33 | Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix); 34 | }); 35 | } 36 | 37 | bootstrap(); 38 | -------------------------------------------------------------------------------- /apps/api/src/widgets/dto/create-widget.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateWidgetDto {} 2 | -------------------------------------------------------------------------------- /apps/api/src/widgets/dto/update-widget.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateWidgetDto } from './create-widget.dto'; 3 | 4 | export class UpdateWidgetDto extends PartialType(CreateWidgetDto) {} 5 | -------------------------------------------------------------------------------- /apps/api/src/widgets/entities/widget.entity.ts: -------------------------------------------------------------------------------- 1 | export class Widget {} 2 | -------------------------------------------------------------------------------- /apps/api/src/widgets/widgets.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WidgetsController } from './widgets.controller'; 3 | import { WidgetsService } from './widgets.service'; 4 | 5 | describe('WidgetsController', () => { 6 | let controller: WidgetsController; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | controllers: [WidgetsController], 11 | providers: [WidgetsService], 12 | }).compile(); 13 | 14 | controller = module.get(WidgetsController); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(controller).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /apps/api/src/widgets/widgets.controller.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@fem/api-interfaces'; 2 | import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; 3 | import { WidgetsService } from './widgets.service'; 4 | 5 | @Controller('widgets') 6 | export class WidgetsController { 7 | constructor(private readonly widgetsService: WidgetsService) {} 8 | 9 | @Post() 10 | create(@Body() widget: Widget) { 11 | return this.widgetsService.create(widget); 12 | } 13 | 14 | @Get() 15 | findAll() { 16 | return this.widgetsService.findAll(); 17 | } 18 | 19 | @Get(':id') 20 | findOne(@Param('id') id: string) { 21 | return this.widgetsService.findOne(id); 22 | } 23 | 24 | @Put(':id') 25 | update(@Param('id') id: string, @Body() widget: Widget) { 26 | return this.widgetsService.update(id, widget); 27 | } 28 | 29 | @Delete(':id') 30 | remove(@Param('id') id: string) { 31 | return this.widgetsService.remove(id); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/src/widgets/widgets.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WidgetsService } from './widgets.service'; 3 | import { WidgetsController } from './widgets.controller'; 4 | 5 | @Module({ 6 | controllers: [WidgetsController], 7 | providers: [WidgetsService] 8 | }) 9 | export class WidgetsModule {} 10 | -------------------------------------------------------------------------------- /apps/api/src/widgets/widgets.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { WidgetsService } from './widgets.service'; 3 | 4 | describe('WidgetsService', () => { 5 | let service: WidgetsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [WidgetsService], 10 | }).compile(); 11 | 12 | service = module.get(WidgetsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/api/src/widgets/widgets.service.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@fem/api-interfaces'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | @Injectable() 6 | export class WidgetsService { 7 | mockWidgets: Widget[] = [ 8 | { id: '1', title: 'Nest Widget 01', description: 'This is a Nest widget' }, 9 | { id: '2', title: 'Nest Widget 02', description: 'This is a Nest widget' }, 10 | { id: '3', title: 'Nest Widget 03', description: 'This is a Nest widget' }, 11 | ]; 12 | 13 | findAll() { 14 | return this.mockWidgets; 15 | } 16 | 17 | findOne(id: string) { 18 | return this.mockWidgets.find((widget) => widget.id === id); 19 | } 20 | 21 | create(widget: Widget) { 22 | this.mockWidgets = [...this.mockWidgets, Object.assign({}, widget, { id: uuidv4() })]; 23 | return this.mockWidgets; 24 | } 25 | 26 | update(id: string, widget: Widget) { 27 | this.mockWidgets = this.mockWidgets.map((w) => (w.id === id ? widget : w)); 28 | return this.mockWidgets; 29 | } 30 | 31 | remove(id: string) { 32 | this.mockWidgets = this.mockWidgets.filter((widget) => widget.id !== id); 33 | return this.mockWidgets; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "emitDecoratorMetadata": true, 7 | "target": "es2015" 8 | }, 9 | "exclude": ["**/*.spec.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "linterOptions": { "exclude": ["!**/*"] }, 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/client-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/client-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/client-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/client-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/client-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('client', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to client!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/client-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript file using Nx helper 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/client-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/client-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | declare namespace Cypress { 12 | interface Chainable { 13 | login(email: string, password: string): void; 14 | } 15 | } 16 | // 17 | // -- This is a parent command -- 18 | Cypress.Commands.add('login', (email, password) => { 19 | console.log('Custom command example: Login', email, password); 20 | }); 21 | // 22 | // -- This is a child command -- 23 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 24 | // 25 | // 26 | // -- This is a dual command -- 27 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 28 | // 29 | // 30 | // -- This will overwrite an existing command -- 31 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 32 | -------------------------------------------------------------------------------- /apps/client-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/client-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/client-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/client-e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "linterOptions": { "exclude": ["!**/*"] }, 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/client/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /apps/client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'client', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/apps/client', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | {{ link.icon }} 18 | {{ link.title | uppercase }} 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | {{widget.title}} 28 | This is a widget. 29 | 30 | Learn to Love Coding 31 | 32 |

Description

33 |

{{widget.description}}

34 |
35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /apps/client/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/client/src/app/app.component.scss -------------------------------------------------------------------------------- /apps/client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | declarations: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have as title 'client'`, () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual('client'); 21 | }); 22 | 23 | it('should render title', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement; 27 | expect(compiled.querySelector('h1').textContent).toContain( 28 | 'Welcome to client!' 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /apps/client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | import { WidgetsService } from '@fem/core-data'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'fem-root', 8 | templateUrl: './app.component.html', 9 | styleUrls: ['./app.component.scss'], 10 | }) 11 | export class AppComponent implements OnInit { 12 | links = [ 13 | { path: '/', icon: 'home', title: 'home' }, 14 | { path: '/widgets', icon: 'view_list', title: 'widgets' }, 15 | ]; 16 | 17 | widgets$: Observable; 18 | 19 | constructor(private widgetsService: WidgetsService) {} 20 | 21 | ngOnInit(): void { 22 | this.loadWidgets(); 23 | } 24 | 25 | loadWidgets() { 26 | this.widgets$ = this.widgetsService.all(); 27 | } 28 | 29 | logout() { } 30 | 31 | toggleSidenav() { } 32 | } 33 | -------------------------------------------------------------------------------- /apps/client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { RouterModule } from '@angular/router'; 6 | import { CoreDataModule } from '@fem/core-data'; 7 | import { CoreStateModule } from '@fem/core-state'; 8 | import { MaterialModule } from '@fem/material'; 9 | import { UiToolbarModule } from '@fem/ui-toolbar'; 10 | 11 | import { AppComponent } from './app.component'; 12 | 13 | @NgModule({ 14 | declarations: [AppComponent], 15 | imports: [ 16 | BrowserAnimationsModule, 17 | BrowserModule, 18 | CoreDataModule, 19 | CoreStateModule, 20 | HttpClientModule, 21 | RouterModule.forRoot([]), 22 | MaterialModule, 23 | UiToolbarModule, 24 | ], 25 | providers: [], 26 | bootstrap: [AppComponent], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /apps/client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/client/src/assets/logo.png -------------------------------------------------------------------------------- /apps/client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /apps/client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/client/src/favicon.ico -------------------------------------------------------------------------------- /apps/client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /apps/client/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'; 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 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /apps/client/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | display: flex; 11 | font-family: Roboto, 'Helvetica Neue', sans-serif; 12 | } 13 | 14 | fem-root { 15 | display: flex; 16 | flex-direction: column; 17 | flex: 1; 18 | } 19 | 20 | fem-toolbar { 21 | z-index: 200; 22 | } 23 | 24 | mat-sidenav { 25 | width: 300px; 26 | background-color: #eceff1 !important; 27 | border-right: solid 1px rgba(0,0,0,.12); 28 | 29 | .logo { 30 | margin: 0 auto; 31 | 32 | img { 33 | margin-top: 20px; 34 | margin-bottom: 20px; 35 | width: 50%; 36 | margin-left: 25%; 37 | margin-right: 25%; 38 | height: auto; 39 | } 40 | } 41 | } 42 | 43 | mat-card { 44 | border-radius: 0px !important; 45 | width: 300px; 46 | margin: 20px; 47 | } 48 | 49 | .mat-sidenav-container { 50 | background: #f5f5f5; 51 | flex: 1; 52 | } 53 | 54 | .mat-list-item { 55 | mat-icon { 56 | font-size: 28px; 57 | margin-right: 15px; 58 | } 59 | } 60 | 61 | .nav-link { 62 | &.mat-button { 63 | border-radius: 0px !important; 64 | } 65 | 66 | span { 67 | margin: 5px 0; 68 | font-size: 18px; 69 | } 70 | 71 | display: flex !important; 72 | align-items: center; 73 | padding-top: 15px; 74 | padding-bottom: 15px; 75 | } 76 | 77 | .nav-link:hover { 78 | color: #fff; 79 | } 80 | 81 | .nav-link.active { 82 | color: #fff; 83 | } 84 | 85 | .container { 86 | flex: 1 1 100%; 87 | } 88 | 89 | .app-container { 90 | flex-wrap: wrap; 91 | display: flex; 92 | 93 | margin: 10px; 94 | 95 | mat-card { 96 | margin-bottom: 10px; 97 | } 98 | } 99 | 100 | .component-container { 101 | margin: 10px; 102 | flex-wrap: wrap; 103 | display: flex; 104 | justify-content: space-around; 105 | 106 | .list-component { 107 | width: 49%; 108 | } 109 | 110 | .details-component { 111 | width: 49%; 112 | 113 | mat-form-field { 114 | width: 100%; 115 | } 116 | 117 | mat-card-header { 118 | display: flex; 119 | justify-content: space-between; 120 | align-items: center; 121 | } 122 | } 123 | } 124 | 125 | mat-list-item:not(:first-of-type) { 126 | border-top: 1px solid #efefef; 127 | } 128 | 129 | mat-form-field { 130 | width: 100%; 131 | } 132 | 133 | mat-list-item:hover { 134 | cursor: pointer; 135 | background: whitesmoke; 136 | } 137 | 138 | mat-list-item:not(:first-of-type) { 139 | border-top: 1px solid #efefef; 140 | } 141 | 142 | mat-toolbar { 143 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 144 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 145 | z-index: 900; 146 | 147 | > .mat-mini-fab { 148 | margin-right: 10px; 149 | } 150 | 151 | .spacer { 152 | flex: 1 1 auto; 153 | } 154 | 155 | .title { 156 | vertical-align: middle; 157 | margin-left: 10px; 158 | } 159 | } 160 | 161 | .flex { 162 | display: flex !important; 163 | 164 | &--column { 165 | @extend .flex; 166 | flex-direction: column; 167 | } 168 | } 169 | 170 | .justify-content-between { 171 | @extend .flex; 172 | justify-content: space-between; 173 | } 174 | 175 | .clickable { 176 | cursor: pointer; 177 | } 178 | 179 | .fill-remaining-space { 180 | flex: 1 1 auto; 181 | } 182 | 183 | .full-width { 184 | width: 100%; 185 | } 186 | 187 | .text-center { 188 | text-align: center; 189 | } 190 | 191 | .text-right { 192 | text-align: right; 193 | } 194 | 195 | .text-left { 196 | text-align: left; 197 | } 198 | 199 | .text-ellipses { 200 | overflow: hidden; 201 | text-overflow: ellipsis; 202 | } 203 | 204 | .no-wrap { 205 | white-space: nowrap; 206 | } 207 | -------------------------------------------------------------------------------- /apps/client/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /apps/client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/client/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/client/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/dashboard-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/dashboard-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('dashboard', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to dashboard!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { preprocessTypescript } = require('@nrwl/cypress/plugins/preprocessor'); 15 | 16 | module.exports = (on, config) => { 17 | // `on` is used to hook into various events Cypress emits 18 | // `config` is the resolved Cypress config 19 | 20 | // Preprocess Typescript file using Nx helper 21 | on('file:preprocessor', preprocessTypescript(config)); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | declare namespace Cypress { 12 | interface Chainable { 13 | login(email: string, password: string): void; 14 | } 15 | } 16 | // 17 | // -- This is a parent command -- 18 | Cypress.Commands.add('login', (email, password) => { 19 | console.log('Custom command example: Login', email, password); 20 | }); 21 | // 22 | // -- This is a child command -- 23 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 24 | // 25 | // 26 | // -- This is a dual command -- 27 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 28 | // 29 | // 30 | // -- This will overwrite an existing command -- 31 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 32 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/dashboard-e2e/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "linterOptions": { "exclude": ["!**/*"] }, 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /apps/dashboard/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'dashboard', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/apps/dashboard', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/dashboard/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3333", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 21 | {{ link.icon }} 22 | {{ link.title | uppercase }} 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/app/app.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { async, TestBed } from '@angular/core/testing'; 3 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { MaterialModule } from '@fem/material'; 6 | import { UiToolbarModule } from '@fem/ui-toolbar'; 7 | import { AppComponent } from './app.component'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [AppComponent], 13 | imports: [ 14 | HttpClientTestingModule, 15 | UiToolbarModule, 16 | MaterialModule, 17 | RouterTestingModule, 18 | NoopAnimationsModule, 19 | ], 20 | }).compileComponents(); 21 | })); 22 | 23 | it('should create the app', () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'fem-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'], 7 | }) 8 | export class AppComponent { 9 | links = [ 10 | { path: '/', icon: 'home', title: 'home' }, 11 | { path: '/widgets', icon: 'view_list', title: 'widgets' }, 12 | ]; 13 | 14 | logout() { } 15 | 16 | toggleSidenav() { } 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { CoreDataModule } from '@fem/core-data'; 7 | import { CoreStateModule } from '@fem/core-state'; 8 | import { MaterialModule } from '@fem/material'; 9 | import { UiToolbarModule } from '@fem/ui-toolbar'; 10 | import { AppComponent } from './app.component'; 11 | import { HomeComponent } from './home/home.component'; 12 | import { RoutingModule } from './routing.module'; 13 | import { WidgetDetailsComponent } from './widgets/widget-details/widget-details.component'; 14 | import { WidgetsListComponent } from './widgets/widgets-list/widgets-list.component'; 15 | import { WidgetsComponent } from './widgets/widgets.component'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | AppComponent, 20 | HomeComponent, 21 | WidgetDetailsComponent, 22 | WidgetsComponent, 23 | WidgetsListComponent, 24 | ], 25 | imports: [ 26 | BrowserAnimationsModule, 27 | BrowserModule, 28 | CoreDataModule, 29 | CoreStateModule, 30 | FormsModule, 31 | HttpClientModule, 32 | MaterialModule, 33 | RoutingModule, 34 | UiToolbarModule, 35 | ], 36 | providers: [], 37 | bootstrap: [AppComponent], 38 | }) 39 | export class AppModule {} 40 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/app/home/home.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MaterialModule } from '@fem/material'; 4 | import { WidgetsListComponent } from '../widgets/widgets-list/widgets-list.component'; 5 | import { HomeComponent } from './home.component'; 6 | 7 | describe('HomeComponent', () => { 8 | let component: HomeComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async () => { 12 | await TestBed.configureTestingModule({ 13 | declarations: [ HomeComponent, WidgetsListComponent ], 14 | imports: [MaterialModule, NoopAnimationsModule] 15 | }) 16 | .compileComponents(); 17 | }); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(HomeComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | import { WidgetsFacade } from '@fem/core-state'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'fem-home', 8 | templateUrl: './home.component.html', 9 | styleUrls: ['./home.component.scss'] 10 | }) 11 | export class HomeComponent implements OnInit{ 12 | allWidgets$: Observable = this.widgetsFacade.allWidgets$; 13 | 14 | constructor(private widgetsFacade: WidgetsFacade) {} 15 | 16 | ngOnInit(): void { 17 | this.loadWidgets(); 18 | } 19 | 20 | loadWidgets() { 21 | this.widgetsFacade.loadWidgets(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { HomeComponent } from './home/home.component'; 4 | import { WidgetsComponent } from './widgets/widgets.component'; 5 | 6 | const routes: Routes = [ 7 | { path: '', component: HomeComponent }, 8 | { path: 'widgets', component: WidgetsComponent }, 9 | { path: '**', redirectTo: '/' }, 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes)], 14 | exports: [RouterModule], 15 | }) 16 | export class RoutingModule { } 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widget-details/widget-details.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Editing {{ originalTitle }} 5 | 6 | 7 | Select Widget 8 | 9 | 10 |
11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widget-details/widget-details.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/app/widgets/widget-details/widget-details.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widget-details/widget-details.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { MaterialModule } from '@fem/material'; 5 | 6 | import { WidgetDetailsComponent } from './widget-details.component'; 7 | 8 | describe('WidgetDetailsComponent', () => { 9 | let component: WidgetDetailsComponent; 10 | let fixture: ComponentFixture; 11 | 12 | beforeEach(async () => { 13 | await TestBed.configureTestingModule({ 14 | declarations: [WidgetDetailsComponent], 15 | imports: [FormsModule, MaterialModule, NoopAnimationsModule], 16 | }).compileComponents(); 17 | }); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(WidgetDetailsComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widget-details/widget-details.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | 4 | @Component({ 5 | selector: 'fem-widget-details', 6 | templateUrl: './widget-details.component.html', 7 | styleUrls: ['./widget-details.component.scss'] 8 | }) 9 | export class WidgetDetailsComponent { 10 | currentWidget: Widget; 11 | originalTitle = ''; 12 | @Input() set widget(value: Widget) { 13 | if(value) this.originalTitle = value.title; 14 | this.currentWidget = {...value}; 15 | }; 16 | @Output() saved = new EventEmitter; 17 | @Output() cancelled = new EventEmitter; 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets-list/widgets-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Widgets 4 | 5 | 6 | 7 | 11 | {{ widget.title }} 12 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets-list/widgets-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/app/widgets/widgets-list/widgets-list.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets-list/widgets-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MaterialModule } from '@fem/material'; 4 | import { WidgetsListComponent } from './widgets-list.component'; 5 | 6 | describe('WidgetsListComponent', () => { 7 | let component: WidgetsListComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | declarations: [ WidgetsListComponent ], 13 | imports: [MaterialModule, NoopAnimationsModule], 14 | }) 15 | .compileComponents(); 16 | }); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(WidgetsListComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets-list/widgets-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | 4 | @Component({ 5 | selector: 'fem-widgets-list', 6 | templateUrl: './widgets-list.component.html', 7 | styleUrls: ['./widgets-list.component.scss'] 8 | }) 9 | export class WidgetsListComponent { 10 | @Input() widgets: Widget[]; 11 | @Input() readonly = false; 12 | @Output() selected = new EventEmitter(); 13 | @Output() deleted = new EventEmitter(); 14 | } 15 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 7 |
8 |
9 | 12 | 13 |
14 |
15 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/app/widgets/widgets.component.scss -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { RouterTestingModule } from '@angular/router/testing'; 6 | import { CoreDataModule } from '@fem/core-data'; 7 | import { CoreStateModule, WidgetsFacade } from '@fem/core-state'; 8 | import { MaterialModule } from '@fem/material'; 9 | import { of } from 'rxjs'; 10 | 11 | import { WidgetDetailsComponent } from './widget-details/widget-details.component'; 12 | import { WidgetsListComponent } from './widgets-list/widgets-list.component'; 13 | import { WidgetsComponent } from './widgets.component'; 14 | 15 | export const mockWidgetsFacade = { 16 | loadWidgets: () => {}, 17 | selectWidget: () => {}, 18 | deleteWidget: () => {}, 19 | updateWidget: () => {}, 20 | createWidget: () => {}, 21 | mutations$: of(true), 22 | }; 23 | 24 | describe('WidgetsComponent', () => { 25 | let component: WidgetsComponent; 26 | let fixture: ComponentFixture; 27 | 28 | beforeEach(async () => { 29 | await TestBed.configureTestingModule({ 30 | declarations: [ 31 | WidgetsComponent, 32 | WidgetDetailsComponent, 33 | WidgetsListComponent, 34 | ], 35 | imports: [ 36 | CoreDataModule, 37 | CoreStateModule, 38 | FormsModule, 39 | MaterialModule, 40 | HttpClientTestingModule, 41 | NoopAnimationsModule, 42 | RouterTestingModule, 43 | ], 44 | providers: [ 45 | { provide: WidgetsFacade, useValue: mockWidgetsFacade }, 46 | ] 47 | }) 48 | .compileComponents(); 49 | }); 50 | 51 | beforeEach(() => { 52 | fixture = TestBed.createComponent(WidgetsComponent); 53 | component = fixture.componentInstance; 54 | fixture.detectChanges(); 55 | }); 56 | 57 | it('should create', () => { 58 | expect(component).toBeTruthy(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/widgets/widgets.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | import { WidgetsFacade } from '@fem/core-state'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'fem-widgets', 8 | templateUrl: './widgets.component.html', 9 | styleUrls: ['./widgets.component.scss'], 10 | }) 11 | export class WidgetsComponent implements OnInit { 12 | allWidgets$: Observable = this.widgetsFacade.allWidgets$; 13 | selectedWidget$: Observable = this.widgetsFacade.selectedWidget$; 14 | 15 | constructor(private widgetsFacade: WidgetsFacade) {} 16 | 17 | ngOnInit(): void { 18 | this.reset(); 19 | this.widgetsFacade.mutations$.subscribe((_) => this.reset()) 20 | } 21 | 22 | reset() { 23 | this.loadWidgets(); 24 | this.selectWidget(null); 25 | } 26 | 27 | resetForm() { 28 | this.selectWidget(null); 29 | } 30 | 31 | selectWidget(widget: Widget) { 32 | this.widgetsFacade.selectWidget(widget?.id); 33 | } 34 | 35 | loadWidgets() { 36 | this.widgetsFacade.loadWidgets(); 37 | } 38 | 39 | saveWidget(widget: Widget) { 40 | this.widgetsFacade.saveWidget(widget); 41 | } 42 | 43 | deleteWidget(widget: Widget) { 44 | this.widgetsFacade.deleteWidget(widget); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/dashboard/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/dashboard/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/assets/logo.png -------------------------------------------------------------------------------- /apps/dashboard/src/assets/screenshots/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/assets/screenshots/api.png -------------------------------------------------------------------------------- /apps/dashboard/src/assets/screenshots/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/assets/screenshots/app.png -------------------------------------------------------------------------------- /apps/dashboard/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiEndpoint: 'http://localhost:3333/api/' 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/apps/dashboard/src/favicon.ico -------------------------------------------------------------------------------- /apps/dashboard/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dashboard 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /apps/dashboard/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /apps/dashboard/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'; 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 | * APPLICATION IMPORTS 62 | */ 63 | -------------------------------------------------------------------------------- /apps/dashboard/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | display: flex; 11 | font-family: Roboto, 'Helvetica Neue', sans-serif; 12 | } 13 | 14 | fem-root { 15 | display: flex; 16 | flex-direction: column; 17 | flex: 1; 18 | } 19 | 20 | fem-toolbar { 21 | z-index: 200; 22 | } 23 | 24 | mat-sidenav { 25 | width: 300px; 26 | background-color: #eceff1 !important; 27 | border-right: solid 1px rgba(0,0,0,.12); 28 | 29 | .logo { 30 | margin: 0 auto; 31 | 32 | img { 33 | margin-top: 20px; 34 | margin-bottom: 20px; 35 | width: 50%; 36 | margin-left: 25%; 37 | margin-right: 25%; 38 | height: auto; 39 | } 40 | } 41 | } 42 | 43 | mat-card { 44 | border-radius: 0px !important; 45 | } 46 | 47 | .mat-sidenav-container { 48 | background: #f5f5f5; 49 | flex: 1; 50 | } 51 | 52 | .mat-list-item { 53 | mat-icon { 54 | font-size: 28px; 55 | margin-right: 15px; 56 | } 57 | } 58 | 59 | .nav-link { 60 | &.mat-button { 61 | border-radius: 0px !important; 62 | } 63 | 64 | span { 65 | margin: 5px 0; 66 | font-size: 18px; 67 | } 68 | 69 | display: flex !important; 70 | align-items: center; 71 | padding-top: 15px; 72 | padding-bottom: 15px; 73 | } 74 | 75 | .nav-link:hover { 76 | color: #fff; 77 | } 78 | 79 | .nav-link.active { 80 | color: #fff; 81 | } 82 | 83 | .container { 84 | flex: 1 1 100%; 85 | } 86 | 87 | .home-container { 88 | margin: 10px; 89 | 90 | mat-card { 91 | margin-bottom: 10px; 92 | } 93 | } 94 | 95 | .component-container { 96 | margin: 10px; 97 | flex-wrap: wrap; 98 | display: flex; 99 | justify-content: space-around; 100 | 101 | .list-component { 102 | width: 49%; 103 | } 104 | 105 | .details-component { 106 | width: 49%; 107 | 108 | mat-form-field { 109 | width: 100%; 110 | } 111 | 112 | mat-card-header { 113 | display: flex; 114 | justify-content: space-between; 115 | align-items: center; 116 | } 117 | } 118 | } 119 | 120 | mat-list-item:not(:first-of-type) { 121 | border-top: 1px solid #efefef; 122 | } 123 | 124 | mat-form-field { 125 | width: 100%; 126 | } 127 | 128 | mat-list-item:hover { 129 | cursor: pointer; 130 | background: whitesmoke; 131 | } 132 | 133 | mat-list-item:not(:first-of-type) { 134 | border-top: 1px solid #efefef; 135 | } 136 | 137 | mat-toolbar { 138 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 139 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 140 | z-index: 900; 141 | 142 | > .mat-mini-fab { 143 | margin-right: 10px; 144 | } 145 | 146 | .spacer { 147 | flex: 1 1 auto; 148 | } 149 | 150 | .title { 151 | vertical-align: middle; 152 | margin-left: 10px; 153 | } 154 | } 155 | 156 | .flex { 157 | display: flex !important; 158 | 159 | &--column { 160 | @extend .flex; 161 | flex-direction: column; 162 | } 163 | } 164 | 165 | .justify-content-between { 166 | @extend .flex; 167 | justify-content: space-between; 168 | } 169 | 170 | .clickable { 171 | cursor: pointer; 172 | } 173 | 174 | .fill-remaining-space { 175 | flex: 1 1 auto; 176 | } 177 | 178 | .full-width { 179 | width: 100%; 180 | } 181 | 182 | .text-center { 183 | text-align: center; 184 | } 185 | 186 | .text-right { 187 | text-align: right; 188 | } 189 | 190 | .text-left { 191 | text-align: left; 192 | } 193 | 194 | .text-ellipses { 195 | overflow: hidden; 196 | text-overflow: ellipsis; 197 | } 198 | 199 | .no-wrap { 200 | white-space: nowrap; 201 | } 202 | -------------------------------------------------------------------------------- /apps/dashboard/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "types": ["jest", "node"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | }, 12 | { 13 | "path": "./tsconfig.editor.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/dashboard/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/dashboard/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require("fs"); 25 | const os = require("os"); 26 | const cp = require("child_process"); 27 | const isWindows = os.platform() === "win32"; 28 | let output; 29 | try { 30 | output = require("@nrwl/workspace").output; 31 | } catch (e) { 32 | console.warn( 33 | "Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed." 34 | ); 35 | process.exit(0); 36 | } 37 | 38 | /** 39 | * Paths to files being patched 40 | */ 41 | const angularCLIInitPath = "node_modules/@angular/cli/lib/cli/index.js"; 42 | 43 | /** 44 | * Patch index.js to warn you if you invoke the undecorated Angular CLI. 45 | */ 46 | function patchAngularCLI(initPath) { 47 | const angularCLIInit = fs.readFileSync(initPath, "utf-8").toString(); 48 | 49 | if (!angularCLIInit.includes("NX_CLI_SET")) { 50 | fs.writeFileSync( 51 | initPath, 52 | ` 53 | if (!process.env['NX_CLI_SET']) { 54 | const { output } = require('@nrwl/workspace'); 55 | output.warn({ title: 'The Angular CLI was invoked instead of the Nx CLI. Use "npx ng [command]" or "nx [command]" instead.' }); 56 | } 57 | ${angularCLIInit} 58 | ` 59 | ); 60 | } 61 | } 62 | 63 | /** 64 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 65 | * invoke the Nx CLI and get the benefits of computation caching. 66 | */ 67 | function symlinkNgCLItoNxCLI() { 68 | try { 69 | const ngPath = "./node_modules/.bin/ng"; 70 | const nxPath = "./node_modules/.bin/nx"; 71 | if (isWindows) { 72 | /** 73 | * This is the most reliable way to create symlink-like behavior on Windows. 74 | * Such that it works in all shells and works with npx. 75 | */ 76 | ["", ".cmd", ".ps1"].forEach((ext) => { 77 | if (fs.existsSync(nxPath + ext)) 78 | fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 79 | }); 80 | } else { 81 | // If unix-based, symlink 82 | cp.execSync(`ln -sf ./nx ${ngPath}`); 83 | } 84 | } catch (e) { 85 | output.error({ 86 | title: 87 | "Unable to create a symlink from the Angular CLI to the Nx CLI:" + 88 | e.message, 89 | }); 90 | throw e; 91 | } 92 | } 93 | 94 | try { 95 | symlinkNgCLItoNxCLI(); 96 | patchAngularCLI(angularCLIInitPath); 97 | output.log({ 98 | title: "Angular CLI has been decorated to enable computation caching.", 99 | }); 100 | } catch (e) { 101 | output.error({ 102 | title: "Decoration of the Angular CLI did not complete successfully", 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/apps/dashboard', 4 | '/apps/api', 5 | '/libs/api-interfaces', 6 | '/libs/core-data', 7 | '/libs/core-state', 8 | '/libs/material', 9 | '/apps/client', 10 | '/libs/ui-toolbar', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/libs/.gitkeep -------------------------------------------------------------------------------- /libs/api-interfaces/README.md: -------------------------------------------------------------------------------- 1 | # api-interfaces 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `ng test api-interfaces` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/api-interfaces/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'api-interfaces', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 13 | coverageDirectory: '../../coverage/libs/api-interfaces', 14 | }; 15 | -------------------------------------------------------------------------------- /libs/api-interfaces/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Message, Widget } from './lib/api-interfaces'; 2 | -------------------------------------------------------------------------------- /libs/api-interfaces/src/lib/api-interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | message: string; 3 | } 4 | 5 | export interface BaseEntity { 6 | id: string | null; 7 | } 8 | 9 | export interface Widget extends BaseEntity { 10 | title: string; 11 | description: string; 12 | } 13 | -------------------------------------------------------------------------------- /libs/api-interfaces/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/api-interfaces/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/api-interfaces/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/api-interfaces/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "linterOptions": { "exclude": ["!**/*"] }, 4 | "rules": {} 5 | } 6 | -------------------------------------------------------------------------------- /libs/core-data/README.md: -------------------------------------------------------------------------------- 1 | # core-data 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test core-data` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/core-data/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'core-data', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/libs/core-data', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/core-data/src/index.ts: -------------------------------------------------------------------------------- 1 | export { CoreDataModule } from './lib/core-data.module'; 2 | export { WidgetsService } from './lib/services/widgets/widgets.service'; 3 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/core-data.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | @NgModule({ 5 | imports: [CommonModule], 6 | }) 7 | export class CoreDataModule {} 8 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/services/widgets/widgets.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { WidgetsService } from './widgets.service'; 4 | 5 | describe('WidgetsService', () => { 6 | let service: WidgetsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(WidgetsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /libs/core-data/src/lib/services/widgets/widgets.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { environment } from '@env/environment'; 4 | import { Widget } from '@fem/api-interfaces'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class WidgetsService { 10 | model = 'widgets'; 11 | 12 | constructor(private http: HttpClient) {} 13 | 14 | all() { 15 | return this.http.get(this.getUrl()); 16 | } 17 | 18 | find(id: string) { 19 | return this.http.get(this.getUrlWithId(id)); 20 | } 21 | 22 | create(widget: Widget) { 23 | return this.http.post(this.getUrl(), widget); 24 | } 25 | 26 | update(widget: Widget) { 27 | return this.http.put(this.getUrlWithId(widget.id), widget); 28 | } 29 | 30 | delete(widget: Widget) { 31 | return this.http.delete(this.getUrlWithId(widget.id)); 32 | } 33 | 34 | private getUrl() { 35 | return `${environment.apiEndpoint}${this.model}`; 36 | } 37 | 38 | private getUrlWithId(id) { 39 | return `${this.getUrl()}/${id}`; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /libs/core-data/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/core-data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/core-data/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/core-data/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/core-data/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/core-state/README.md: -------------------------------------------------------------------------------- 1 | # core-state 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test core-state` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/core-state/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'core-state', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/libs/core-state', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/core-state/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/widgets/widgets.actions'; 2 | export * from './lib/widgets/widgets.reducer'; 3 | export * from './lib/widgets/widgets.selectors'; 4 | export * from './lib/widgets/widgets.facade'; 5 | export * from './lib/core-state.module'; 6 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/core-state.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { EffectsModule } from '@ngrx/effects'; 4 | import { StoreRouterConnectingModule } from '@ngrx/router-store'; 5 | import { RootStoreConfig, StoreModule } from '@ngrx/store'; 6 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 7 | 8 | import { reducers } from '.'; 9 | import { WidgetsEffects } from './widgets/widgets.effects'; 10 | import { WidgetsFacade } from './widgets/widgets.facade'; 11 | 12 | const STORE_NAME = 'fem-store'; 13 | const storeConfig: RootStoreConfig = { 14 | runtimeChecks: { 15 | strictActionImmutability: true, 16 | strictActionSerializability: true, 17 | strictStateImmutability: true, 18 | strictStateSerializability: true, 19 | }, 20 | }; 21 | 22 | @NgModule({ 23 | imports: [ 24 | CommonModule, 25 | StoreModule.forRoot(reducers, storeConfig), 26 | EffectsModule.forRoot([WidgetsEffects]), 27 | StoreDevtoolsModule.instrument({ maxAge: 25, name: STORE_NAME }), 28 | StoreRouterConnectingModule.forRoot({ stateKey: 'router' }), 29 | ], 30 | providers: [WidgetsFacade], 31 | }) 32 | export class CoreStateModule {} 33 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { Params } from '@angular/router'; 2 | import * as fromRouter from '@ngrx/router-store'; 3 | import { ActionReducerMap } from '@ngrx/store'; 4 | import * as fromWidgets from './widgets/widgets.reducer'; 5 | 6 | export interface RouterStateUrl { 7 | url: string; 8 | queryParams: Params; 9 | params: Params; 10 | } 11 | 12 | // --------------------------------------- 13 | // Core State and Reducers 14 | // --------------------------------------- 15 | 16 | export interface AppState { 17 | router: fromRouter.RouterReducerState; 18 | [fromWidgets.WIDGETS_FEATURE_KEY]: fromWidgets.WidgetsState; 19 | } 20 | 21 | export const reducers: ActionReducerMap = { 22 | router: fromRouter.routerReducer, 23 | [fromWidgets.WIDGETS_FEATURE_KEY]: fromWidgets.widgetsReducer, 24 | }; 25 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.actions.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@fem/api-interfaces'; 2 | import { createAction, props } from '@ngrx/store'; 3 | 4 | export const resetSelectedWidget = createAction('[Widgets] Reset Selected Widget'); 5 | export const resetWidgets = createAction('[Widgets] Reset Widgets'); 6 | 7 | // Select Widget 8 | export const selectWidget = createAction( 9 | '[Widgets] Select Widget', 10 | props<{ selectedId: string }>() 11 | ); 12 | 13 | // Load Widgets 14 | export const loadWidgets = createAction('[Widgets] Load Widgets'); 15 | 16 | export const loadWidgetsSuccess = createAction( 17 | '[Widgets] Load Widgets Success', 18 | props<{ widgets: Widget[] }>() 19 | ); 20 | 21 | export const loadWidgetsFailure = createAction( 22 | '[Widgets] Load Widgets Failure', 23 | props<{ error: any }>() 24 | ); 25 | 26 | // Load Widget 27 | export const loadWidget = createAction( 28 | '[Widgets] Load Widget', 29 | props<{ widgetId: string }>() 30 | ); 31 | 32 | export const loadWidgetSuccess = createAction( 33 | '[Widgets] Load Widget Success', 34 | props<{ widget: Widget }>() 35 | ); 36 | 37 | export const loadWidgetFailure = createAction( 38 | '[Widgets] Load Widget Failure', 39 | props<{ error: any }>() 40 | ); 41 | 42 | // Create Widget 43 | export const createWidget = createAction( 44 | '[Widgets] Create Widget', 45 | props<{ widget: Widget }>() 46 | ); 47 | 48 | export const createWidgetSuccess = createAction( 49 | '[Widgets] Create Widget Success', 50 | props<{ widget: Widget }>() 51 | ); 52 | 53 | export const createWidgetFailure = createAction( 54 | '[Widgets] Create Widget Failure', 55 | props<{ error: any }>() 56 | ); 57 | 58 | // Update Widget 59 | export const updateWidget = createAction( 60 | '[Widgets] Update Widget', 61 | props<{ widget: Widget }>() 62 | ); 63 | 64 | export const updateWidgetSuccess = createAction( 65 | '[Widgets] Update Widget Success', 66 | props<{ widget: Widget }>() 67 | ); 68 | 69 | export const updateWidgetFailure = createAction( 70 | '[Widgets] Update Widget Failure', 71 | props<{ error: any }>() 72 | ); 73 | 74 | // Delete Widget 75 | export const deleteWidget = createAction( 76 | '[Widgets] Delete Widget', 77 | props<{ widget: Widget }>() 78 | ); 79 | 80 | export const deleteWidgetCancelled = createAction('[Widgets] Delete Widget Cancelled'); 81 | 82 | export const deleteWidgetSuccess = createAction( 83 | '[Widgets] Delete Widget Success', 84 | props<{ widget: Widget }>() 85 | ); 86 | 87 | export const deleteWidgetFailure = createAction( 88 | '[Widgets] Delete Widget Failure', 89 | props<{ error: any }>() 90 | ); 91 | 92 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | 3 | import { Observable } from 'rxjs'; 4 | 5 | import { provideMockActions } from '@ngrx/effects/testing'; 6 | import { provideMockStore } from '@ngrx/store/testing'; 7 | 8 | import { NxModule, DataPersistence } from '@nrwl/angular'; 9 | import { hot } from '@nrwl/angular/testing'; 10 | 11 | import { WidgetsEffects } from './widgets.effects'; 12 | import * as WidgetsActions from './widgets.actions'; 13 | 14 | describe('WidgetsEffects', () => { 15 | let actions: Observable; 16 | let effects: WidgetsEffects; 17 | 18 | beforeEach(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [NxModule.forRoot()], 21 | providers: [ 22 | WidgetsEffects, 23 | DataPersistence, 24 | provideMockActions(() => actions), 25 | provideMockStore(), 26 | ], 27 | }); 28 | 29 | effects = TestBed.get(WidgetsEffects); 30 | }); 31 | 32 | describe('loadWidgets$', () => { 33 | it('should work', () => { 34 | actions = hot('-a-|', { a: WidgetsActions.loadWidgets() }); 35 | 36 | const expected = hot('-a-|', { 37 | a: WidgetsActions.loadWidgetsSuccess({ widgets: [] }), 38 | }); 39 | 40 | expect(effects.loadWidgets$).toBeObservable(expected); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { createEffect, Actions, ofType } from '@ngrx/effects'; 3 | import { fetch, pessimisticUpdate } from '@nrwl/angular'; 4 | import { map } from 'rxjs/operators'; 5 | 6 | import * as fromWidgets from './widgets.reducer'; 7 | import * as WidgetsActions from './widgets.actions'; 8 | import { Widget } from '@fem/api-interfaces'; 9 | import { WidgetsService } from '@fem/core-data'; 10 | 11 | @Injectable() 12 | export class WidgetsEffects { 13 | loadWidgets$ = createEffect(() => this.actions$.pipe( 14 | ofType(WidgetsActions.loadWidgets), 15 | fetch({ 16 | run: (action) => 17 | this.widgetsService 18 | .all() 19 | .pipe( 20 | map((widgets: Widget[]) => 21 | WidgetsActions.loadWidgetsSuccess({ widgets }) 22 | ) 23 | ), 24 | onError: (action, error) => WidgetsActions.loadWidgetsFailure({ error }), 25 | }) 26 | )); 27 | 28 | loadWidget$ = createEffect(() => this.actions$.pipe( 29 | ofType(WidgetsActions.loadWidget), 30 | fetch({ 31 | run: (action) => 32 | this.widgetsService 33 | .find(action.widgetId) 34 | .pipe( 35 | map((widget: Widget) => 36 | WidgetsActions.loadWidgetSuccess({ widget }) 37 | ) 38 | ), 39 | onError: (action, error) => WidgetsActions.loadWidgetFailure({ error }), 40 | }) 41 | )); 42 | 43 | createWidget$ = createEffect(() => this.actions$.pipe( 44 | ofType(WidgetsActions.createWidget), 45 | pessimisticUpdate({ 46 | run: (action) => 47 | this.widgetsService 48 | .create(action.widget) 49 | .pipe( 50 | map((widget: Widget) => 51 | WidgetsActions.createWidgetSuccess({ widget }) 52 | ) 53 | ), 54 | onError: (action, error) => WidgetsActions.createWidgetFailure({ error }), 55 | }) 56 | )); 57 | 58 | updateWidget$ = createEffect(() => this.actions$.pipe( 59 | ofType(WidgetsActions.updateWidget), 60 | pessimisticUpdate({ 61 | run: (action) => 62 | this.widgetsService 63 | .update(action.widget) 64 | .pipe( 65 | map((widget: Widget) => 66 | WidgetsActions.updateWidgetSuccess({ widget }) 67 | ) 68 | ), 69 | onError: (action, error) => WidgetsActions.updateWidgetFailure({ error }), 70 | }) 71 | )); 72 | 73 | deleteWidget$ = createEffect(() => this.actions$.pipe( 74 | ofType(WidgetsActions.deleteWidget), 75 | pessimisticUpdate({ 76 | run: (action) => 77 | this.widgetsService 78 | .delete(action.widget) 79 | .pipe( 80 | map((widget: Widget) => 81 | WidgetsActions.deleteWidgetSuccess({ widget }) 82 | ) 83 | ), 84 | onError: (action, error) => WidgetsActions.deleteWidgetFailure({ error }), 85 | }) 86 | )); 87 | 88 | constructor( 89 | private actions$: Actions, 90 | private widgetsService: WidgetsService 91 | ) {} 92 | } 93 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.facade.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { readFirst } from '@nrwl/angular/testing'; 4 | 5 | import { EffectsModule } from '@ngrx/effects'; 6 | import { StoreModule, Store } from '@ngrx/store'; 7 | 8 | import { NxModule } from '@nrwl/angular'; 9 | 10 | import { WidgetsEntity } from './widgets.models'; 11 | import { WidgetsEffects } from './widgets.effects'; 12 | import { WidgetsFacade } from './widgets.facade'; 13 | 14 | import * as WidgetsSelectors from './widgets.selectors'; 15 | import * as WidgetsActions from './widgets.actions'; 16 | import { 17 | WIDGETS_FEATURE_KEY, 18 | State, 19 | initialState, 20 | reducer, 21 | } from './widgets.reducer'; 22 | 23 | interface TestSchema { 24 | widgets: State; 25 | } 26 | 27 | describe('WidgetsFacade', () => { 28 | let facade: WidgetsFacade; 29 | let store: Store; 30 | const createWidgetsEntity = (id: string, name = '') => 31 | ({ 32 | id, 33 | name: name || `name-${id}`, 34 | } as WidgetsEntity); 35 | 36 | beforeEach(() => {}); 37 | 38 | describe('used in NgModule', () => { 39 | beforeEach(() => { 40 | @NgModule({ 41 | imports: [ 42 | StoreModule.forFeature(WIDGETS_FEATURE_KEY, reducer), 43 | EffectsModule.forFeature([WidgetsEffects]), 44 | ], 45 | providers: [WidgetsFacade], 46 | }) 47 | class CustomFeatureModule {} 48 | 49 | @NgModule({ 50 | imports: [ 51 | NxModule.forRoot(), 52 | StoreModule.forRoot({}), 53 | EffectsModule.forRoot([]), 54 | CustomFeatureModule, 55 | ], 56 | }) 57 | class RootModule {} 58 | TestBed.configureTestingModule({ imports: [RootModule] }); 59 | 60 | store = TestBed.get(Store); 61 | facade = TestBed.get(WidgetsFacade); 62 | }); 63 | 64 | /** 65 | * The initially generated facade::loadAll() returns empty array 66 | */ 67 | it('loadAll() should return empty list with loaded == true', async (done) => { 68 | try { 69 | let list = await readFirst(facade.allWidgets$); 70 | let isLoaded = await readFirst(facade.loaded$); 71 | 72 | expect(list.length).toBe(0); 73 | expect(isLoaded).toBe(false); 74 | 75 | facade.dispatch(WidgetsActions.loadWidgets()); 76 | 77 | list = await readFirst(facade.allWidgets$); 78 | isLoaded = await readFirst(facade.loaded$); 79 | 80 | expect(list.length).toBe(0); 81 | expect(isLoaded).toBe(true); 82 | 83 | done(); 84 | } catch (err) { 85 | done.fail(err); 86 | } 87 | }); 88 | 89 | /** 90 | * Use `loadWidgetsSuccess` to manually update list 91 | */ 92 | it('allWidgets$ should return the loaded list; and loaded flag == true', async (done) => { 93 | try { 94 | let list = await readFirst(facade.allWidgets$); 95 | let isLoaded = await readFirst(facade.loaded$); 96 | 97 | expect(list.length).toBe(0); 98 | expect(isLoaded).toBe(false); 99 | 100 | facade.dispatch( 101 | WidgetsActions.loadWidgetsSuccess({ 102 | widgets: [createWidgetsEntity('AAA'), createWidgetsEntity('BBB')], 103 | }) 104 | ); 105 | 106 | list = await readFirst(facade.allWidgets$); 107 | isLoaded = await readFirst(facade.loaded$); 108 | 109 | expect(list.length).toBe(2); 110 | expect(isLoaded).toBe(true); 111 | 112 | done(); 113 | } catch (err) { 114 | done.fail(err); 115 | } 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.facade.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Widget } from '@fem/api-interfaces'; 3 | import { Action, ActionsSubject, select, Store } from '@ngrx/store'; 4 | import { filter } from 'rxjs/operators'; 5 | import * as WidgetsActions from './widgets.actions'; 6 | import * as WidgetsSelectors from './widgets.selectors'; 7 | 8 | @Injectable() 9 | export class WidgetsFacade { 10 | loaded$ = this.store.pipe(select(WidgetsSelectors.getWidgetsLoaded)); 11 | allWidgets$ = this.store.pipe(select(WidgetsSelectors.getAllWidgets)); 12 | selectedWidget$ = this.store.pipe(select(WidgetsSelectors.getSelectedWidget)); 13 | 14 | mutations$ = this.actions$.pipe( 15 | filter((action: Action) => 16 | action.type === WidgetsActions.createWidget({} as any).type || 17 | action.type === WidgetsActions.updateWidget({} as any).type || 18 | action.type === WidgetsActions.deleteWidget({} as any).type 19 | ) 20 | ); 21 | 22 | constructor(private store: Store, private actions$: ActionsSubject) { } 23 | 24 | selectWidget(selectedId: string) { 25 | this.dispatch(WidgetsActions.selectWidget({ selectedId })); 26 | } 27 | 28 | loadWidgets() { 29 | this.dispatch(WidgetsActions.loadWidgets()); 30 | } 31 | 32 | loadWidget(widgetId: string) { 33 | this.dispatch(WidgetsActions.loadWidget({ widgetId })); 34 | } 35 | 36 | saveWidget(widget: Widget) { 37 | if(widget.id) { 38 | this.updateWidget(widget); 39 | } else { 40 | this.createWidget(widget); 41 | } 42 | } 43 | 44 | createWidget(widget: Widget) { 45 | this.dispatch(WidgetsActions.createWidget({ widget })); 46 | } 47 | 48 | updateWidget(widget: Widget) { 49 | this.dispatch(WidgetsActions.updateWidget({ widget })); 50 | } 51 | 52 | deleteWidget(widget: Widget) { 53 | this.dispatch(WidgetsActions.deleteWidget({ widget })); 54 | } 55 | 56 | dispatch(action: Action) { 57 | this.store.dispatch(action); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.models.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for the 'Widgets' data 3 | */ 4 | export interface WidgetsEntity { 5 | id: string | number; // Primary ID 6 | } 7 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { WidgetsEntity } from './widgets.models'; 2 | import * as WidgetsActions from './widgets.actions'; 3 | import { State, initialState, reducer } from './widgets.reducer'; 4 | 5 | describe('Widgets Reducer', () => { 6 | const createWidgetsEntity = (id: string, name = '') => 7 | ({ 8 | id, 9 | name: name || `name-${id}`, 10 | } as WidgetsEntity); 11 | 12 | beforeEach(() => {}); 13 | 14 | describe('valid Widgets actions', () => { 15 | it('loadWidgetsSuccess should return set the list of known Widgets', () => { 16 | const widgets = [ 17 | createWidgetsEntity('PRODUCT-AAA'), 18 | createWidgetsEntity('PRODUCT-zzz'), 19 | ]; 20 | const action = WidgetsActions.loadWidgetsSuccess({ widgets }); 21 | 22 | const result: State = reducer(initialState, action); 23 | 24 | expect(result.loaded).toBe(true); 25 | expect(result.ids.length).toBe(2); 26 | }); 27 | }); 28 | 29 | describe('unknown action', () => { 30 | it('should return the previous state', () => { 31 | const action = {} as any; 32 | 33 | const result = reducer(initialState, action); 34 | 35 | expect(result).toBe(initialState); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@fem/api-interfaces'; 2 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 3 | import { Action, createReducer, on } from '@ngrx/store'; 4 | 5 | import * as WidgetsActions from './widgets.actions'; 6 | 7 | export const WIDGETS_FEATURE_KEY = 'widgets'; 8 | 9 | export interface WidgetsState extends EntityState { 10 | selectedId?: string | number; // which widgets record has been selected 11 | loaded: boolean; // has the widgets list been loaded 12 | error?: string | null; // last known error (if any) 13 | } 14 | 15 | export interface WidgetsPartialState { 16 | readonly [WIDGETS_FEATURE_KEY]: WidgetsState; 17 | } 18 | 19 | export const widgetsAdapter: EntityAdapter = createEntityAdapter(); 20 | 21 | export const initialWidgetsState: WidgetsState = widgetsAdapter.getInitialState( 22 | { 23 | // set initial required properties 24 | loaded: false, 25 | } 26 | ); 27 | 28 | const onFailure = (state, { error }) => ({ ...state, error }); 29 | 30 | const _widgetsReducer = createReducer( 31 | initialWidgetsState, 32 | on(WidgetsActions.selectWidget, (state, { selectedId }) => 33 | Object.assign({}, state, { selectedId }) 34 | ), 35 | on(WidgetsActions.resetSelectedWidget, (state) => 36 | Object.assign({}, state, { selectedId: null }) 37 | ), 38 | on(WidgetsActions.resetWidgets, (state) => widgetsAdapter.removeAll(state)), 39 | // Load widgets 40 | on(WidgetsActions.loadWidgets, (state) => ({ 41 | ...state, 42 | loaded: false, 43 | error: null, 44 | })), 45 | on(WidgetsActions.loadWidgetsSuccess, (state, { widgets }) => 46 | widgetsAdapter.setAll(widgets, { ...state, loaded: true }) 47 | ), 48 | on(WidgetsActions.loadWidgetsFailure, onFailure), 49 | // Load widget 50 | on(WidgetsActions.loadWidget, (state) => ({ 51 | ...state, 52 | loaded: false, 53 | error: null, 54 | })), 55 | on(WidgetsActions.loadWidgetSuccess, (state, { widget }) => 56 | widgetsAdapter.upsertOne(widget, { ...state, loaded: true }) 57 | ), 58 | on(WidgetsActions.loadWidgetFailure, onFailure), 59 | // Add widget 60 | on(WidgetsActions.createWidgetSuccess, (state, { widget }) => 61 | widgetsAdapter.addOne(widget, state) 62 | ), 63 | on(WidgetsActions.createWidgetFailure, onFailure), 64 | // Update widget 65 | on(WidgetsActions.updateWidgetSuccess, (state, { widget }) => 66 | widgetsAdapter.updateOne({ id: widget.id, changes: widget }, state) 67 | ), 68 | on(WidgetsActions.updateWidgetFailure, onFailure), 69 | // Delete widget 70 | on(WidgetsActions.deleteWidgetSuccess, (state, { widget }) => 71 | widgetsAdapter.removeOne(widget.id, state) 72 | ), 73 | on(WidgetsActions.deleteWidgetFailure, onFailure) 74 | ); 75 | 76 | export function widgetsReducer( 77 | state: WidgetsState | undefined, 78 | action: Action 79 | ) { 80 | return _widgetsReducer(state, action); 81 | } 82 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.selectors.spec.ts: -------------------------------------------------------------------------------- 1 | import { WidgetsEntity } from './widgets.models'; 2 | import { State, widgetsAdapter, initialState } from './widgets.reducer'; 3 | import * as WidgetsSelectors from './widgets.selectors'; 4 | 5 | describe('Widgets Selectors', () => { 6 | const ERROR_MSG = 'No Error Available'; 7 | const getWidgetsId = (it) => it['id']; 8 | const createWidgetsEntity = (id: string, name = '') => 9 | ({ 10 | id, 11 | name: name || `name-${id}`, 12 | } as WidgetsEntity); 13 | 14 | let state; 15 | 16 | beforeEach(() => { 17 | state = { 18 | widgets: widgetsAdapter.setAll( 19 | [ 20 | createWidgetsEntity('PRODUCT-AAA'), 21 | createWidgetsEntity('PRODUCT-BBB'), 22 | createWidgetsEntity('PRODUCT-CCC'), 23 | ], 24 | { 25 | ...initialState, 26 | selectedId: 'PRODUCT-BBB', 27 | error: ERROR_MSG, 28 | loaded: true, 29 | } 30 | ), 31 | }; 32 | }); 33 | 34 | describe('Widgets Selectors', () => { 35 | it('getAllWidgets() should return the list of Widgets', () => { 36 | const results = WidgetsSelectors.getAllWidgets(state); 37 | const selId = getWidgetsId(results[1]); 38 | 39 | expect(results.length).toBe(3); 40 | expect(selId).toBe('PRODUCT-BBB'); 41 | }); 42 | 43 | it('getSelected() should return the selected Entity', () => { 44 | const result = WidgetsSelectors.getSelected(state); 45 | const selId = getWidgetsId(result); 46 | 47 | expect(selId).toBe('PRODUCT-BBB'); 48 | }); 49 | 50 | it("getWidgetsLoaded() should return the current 'loaded' status", () => { 51 | const result = WidgetsSelectors.getWidgetsLoaded(state); 52 | 53 | expect(result).toBe(true); 54 | }); 55 | 56 | it("getWidgetsError() should return the current 'error' state", () => { 57 | const result = WidgetsSelectors.getWidgetsError(state); 58 | 59 | expect(result).toBe(ERROR_MSG); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /libs/core-state/src/lib/widgets/widgets.selectors.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@fem/api-interfaces'; 2 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 3 | import { widgetsAdapter, WidgetsState, WIDGETS_FEATURE_KEY } from './widgets.reducer'; 4 | 5 | // Lookup the 'Widgets' feature state managed by NgRx 6 | export const getWidgetsState = createFeatureSelector< 7 | WidgetsState 8 | >(WIDGETS_FEATURE_KEY); 9 | 10 | const { selectAll, selectEntities } = widgetsAdapter.getSelectors(); 11 | 12 | export const getWidgetsLoaded = createSelector( 13 | getWidgetsState, 14 | (state: WidgetsState) => state.loaded 15 | ); 16 | 17 | export const getWidgetsError = createSelector( 18 | getWidgetsState, 19 | (state: WidgetsState) => state.error 20 | ); 21 | 22 | export const getAllWidgets = createSelector( 23 | getWidgetsState, 24 | (state: WidgetsState) => selectAll(state) 25 | ); 26 | 27 | export const getWidgetsEntities = createSelector( 28 | getWidgetsState, 29 | (state: WidgetsState) => selectEntities(state) 30 | ); 31 | 32 | export const getSelectedWidgetId = createSelector( 33 | getWidgetsState, 34 | (state: WidgetsState) => state.selectedId 35 | ); 36 | 37 | const emptyWidget: Widget = { 38 | id: null, 39 | title: '', 40 | description: '', 41 | }; 42 | 43 | export const getSelectedWidget = createSelector( 44 | getWidgetsEntities, 45 | getSelectedWidgetId, 46 | (entities, selectedId) => { 47 | return selectedId ? entities[selectedId] : emptyWidget; 48 | } 49 | ); 50 | -------------------------------------------------------------------------------- /libs/core-state/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/core-state/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/core-state/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/core-state/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/core-state/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/material/README.md: -------------------------------------------------------------------------------- 1 | # material 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test material` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/material/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'material', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/libs/material', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/material/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/material.module'; 2 | -------------------------------------------------------------------------------- /libs/material/src/lib/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { MatAutocompleteModule } from '@angular/material/autocomplete'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatCheckboxModule } from '@angular/material/checkbox'; 7 | import { MatDialogModule } from '@angular/material/dialog'; 8 | import { MatExpansionModule } from '@angular/material/expansion'; 9 | import { MatFormFieldModule } from '@angular/material/form-field'; 10 | import { MatGridListModule } from '@angular/material/grid-list'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | import { MatInputModule } from '@angular/material/input'; 13 | import { MatListModule } from '@angular/material/list'; 14 | import { MatMenuModule } from '@angular/material/menu'; 15 | import { MatProgressBarModule } from '@angular/material/progress-bar'; 16 | import { MatRadioModule } from '@angular/material/radio'; 17 | import { MatSelectModule } from '@angular/material/select'; 18 | import { MatSidenavModule } from '@angular/material/sidenav'; 19 | import { MatSliderModule } from '@angular/material/slider'; 20 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 21 | import { MatStepperModule } from '@angular/material/stepper'; 22 | import { MatTableModule } from '@angular/material/table'; 23 | import { MatTabsModule } from '@angular/material/tabs'; 24 | import { MatToolbarModule } from '@angular/material/toolbar'; 25 | import { MatTooltipModule } from '@angular/material/tooltip'; 26 | 27 | @NgModule({ 28 | exports: [ 29 | MatAutocompleteModule, 30 | MatButtonModule, 31 | MatButtonToggleModule, 32 | MatCardModule, 33 | MatCheckboxModule, 34 | MatDialogModule, 35 | MatExpansionModule, 36 | MatFormFieldModule, 37 | MatGridListModule, 38 | MatIconModule, 39 | MatInputModule, 40 | MatListModule, 41 | MatMenuModule, 42 | MatProgressBarModule, 43 | MatRadioModule, 44 | MatSelectModule, 45 | MatSidenavModule, 46 | MatSliderModule, 47 | MatSnackBarModule, 48 | MatTableModule, 49 | MatTabsModule, 50 | MatToolbarModule, 51 | MatTooltipModule, 52 | MatStepperModule, 53 | ] 54 | }) 55 | export class MaterialModule {} 56 | -------------------------------------------------------------------------------- /libs/material/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/material/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/material/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/material/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/material/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/ui-toolbar/README.md: -------------------------------------------------------------------------------- 1 | # ui-toolbar 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ui-toolbar` to execute the unit tests. 8 | -------------------------------------------------------------------------------- /libs/ui-toolbar/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ui-toolbar', 3 | preset: '../../jest.preset.js', 4 | setupFilesAfterEnv: ['/src/test-setup.ts'], 5 | globals: { 6 | 'ts-jest': { 7 | tsConfig: '/tsconfig.spec.json', 8 | stringifyContentPathRegex: '\\.(html|svg)$', 9 | astTransformers: { 10 | before: [ 11 | 'jest-preset-angular/build/InlineFilesTransformer', 12 | 'jest-preset-angular/build/StripStylesTransformer', 13 | ], 14 | }, 15 | }, 16 | }, 17 | coverageDirectory: '../../coverage/libs/ui-toolbar', 18 | snapshotSerializers: [ 19 | 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 20 | 'jest-preset-angular/build/AngularSnapshotSerializer.js', 21 | 'jest-preset-angular/build/HTMLCommentSerializer.js', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ToolbarComponent } from './lib/toolbar/toolbar/toolbar.component'; 2 | export { UiToolbarModule } from './lib/ui-toolbar.module'; 3 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar/toolbar.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar/toolbar.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/libs/ui-toolbar/src/lib/toolbar/toolbar/toolbar.component.scss -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar/toolbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToolbarComponent } from './toolbar.component'; 4 | 5 | describe('ToolbarComponent', () => { 6 | let component: ToolbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ToolbarComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToolbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/toolbar/toolbar/toolbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'fem-toolbar', 5 | templateUrl: './toolbar.component.html', 6 | styleUrls: ['./toolbar.component.scss'] 7 | }) 8 | export class ToolbarComponent { 9 | @Output() logout = new EventEmitter(); 10 | @Output() toggleSidenav = new EventEmitter(); 11 | } 12 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/lib/ui-toolbar.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ToolbarComponent } from './toolbar/toolbar/toolbar.component'; 4 | import { MaterialModule } from '@fem/material'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | CommonModule, 9 | MaterialModule, 10 | ], 11 | declarations: [ToolbarComponent], 12 | exports: [ToolbarComponent] 13 | }) 14 | export class UiToolbarModule {} 15 | -------------------------------------------------------------------------------- /libs/ui-toolbar/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "target": "es2015", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"] 11 | }, 12 | "angularCompilerOptions": { 13 | "skipTemplateCodegen": true, 14 | "strictMetadataEmit": true, 15 | "enableResourceInlining": true 16 | }, 17 | "exclude": ["src/test-setup.ts", "**/*.spec.ts"], 18 | "include": ["**/*.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "files": ["src/test-setup.ts"], 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /libs/ui-toolbar/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "fem", "camelCase"], 5 | "component-selector": [true, "element", "fem", "kebab-case"] 6 | }, 7 | "linterOptions": { 8 | "exclude": ["!**/*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "fem", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "implicitDependencies": { 7 | "angular.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.base.json": "*", 13 | "tslint.json": "*", 14 | ".eslintrc.json": "*", 15 | "nx.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "@nrwl/workspace/tasks-runners/default", 20 | "options": { 21 | "cacheableOperations": ["build", "lint", "test", "e2e"] 22 | } 23 | } 24 | }, 25 | "projects": { 26 | "dashboard": { 27 | "tags": [] 28 | }, 29 | "dashboard-e2e": { 30 | "tags": [], 31 | "implicitDependencies": ["dashboard"] 32 | }, 33 | "api": { 34 | "tags": [] 35 | }, 36 | "api-interfaces": { 37 | "tags": [] 38 | }, 39 | "core-data": { 40 | "tags": [] 41 | }, 42 | "core-state": { 43 | "tags": [] 44 | }, 45 | "material": { 46 | "tags": [] 47 | }, 48 | "client": { 49 | "tags": [] 50 | }, 51 | "client-e2e": { 52 | "tags": [], 53 | "implicitDependencies": ["client"] 54 | }, 55 | "ui-toolbar": { 56 | "tags": [] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fem-production-angular", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "nx", 7 | "postinstall": "node ./decorate-angular-cli.js && ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", 8 | "nx": "nx", 9 | "start": "ng serve", 10 | "build": "ng build", 11 | "test": "ng test", 12 | "lint": "nx workspace-lint && ng lint", 13 | "e2e": "ng e2e", 14 | "serve:json": "json-server server/db.json", 15 | "serve:api": "nx run api:serve", 16 | "serve:client": "nx run client:serve --port=4400 --open", 17 | "serve:web": "ng serve --open", 18 | "serve:all": "concurrently \"npm run serve:api\" \"npm run serve:web\"", 19 | "affected:apps": "nx affected:apps", 20 | "affected:libs": "nx affected:libs", 21 | "affected:build": "nx affected:build", 22 | "affected:e2e": "nx affected:e2e", 23 | "affected:test": "nx affected:test", 24 | "affected:lint": "nx affected:lint", 25 | "affected:dep-graph": "nx affected:dep-graph", 26 | "affected": "nx affected", 27 | "format": "nx format:write", 28 | "format:write": "nx format:write", 29 | "format:check": "nx format:check", 30 | "update": "ng update @nrwl/workspace", 31 | "workspace-schematic": "nx workspace-schematic", 32 | "dep-graph": "nx dep-graph", 33 | "help": "nx help" 34 | }, 35 | "private": true, 36 | "dependencies": { 37 | "@angular/animations": "^10.1.0", 38 | "@angular/cdk": "10.2.7", 39 | "@angular/common": "^10.1.0", 40 | "@angular/compiler": "^10.1.0", 41 | "@angular/core": "^10.1.0", 42 | "@angular/forms": "^10.1.0", 43 | "@angular/material": "10.2.7", 44 | "@angular/platform-browser": "^10.1.0", 45 | "@angular/platform-browser-dynamic": "^10.1.0", 46 | "@angular/router": "^10.1.0", 47 | "@nestjs/common": "^7.0.0", 48 | "@nestjs/core": "^7.0.0", 49 | "@nestjs/mapped-types": "^0.1.1", 50 | "@nestjs/platform-express": "^7.0.0", 51 | "@nestjs/swagger": "^4.7.0", 52 | "@ngrx/effects": "10.0.0", 53 | "@ngrx/entity": "10.0.0", 54 | "@ngrx/router-store": "10.0.0", 55 | "@ngrx/store": "10.0.1", 56 | "@nrwl/angular": "10.3.2", 57 | "concurrently": "^5.3.0", 58 | "json-server": "^0.16.2", 59 | "reflect-metadata": "^0.1.13", 60 | "rxjs": "~6.5.5", 61 | "swagger-ui-express": "^4.1.4", 62 | "uuid": "^8.3.1", 63 | "zone.js": "^0.10.2" 64 | }, 65 | "devDependencies": { 66 | "@angular-devkit/build-angular": "~0.1001.3", 67 | "@angular/cli": "~10.1.3", 68 | "@angular/compiler-cli": "^10.1.0", 69 | "@angular/language-service": "^10.1.0", 70 | "@nestjs/schematics": "^7.0.0", 71 | "@nestjs/testing": "^7.0.0", 72 | "@ngrx/schematics": "10.0.0", 73 | "@ngrx/store-devtools": "10.0.0", 74 | "@nrwl/cli": "10.3.2", 75 | "@nrwl/cypress": "10.3.2", 76 | "@nrwl/jest": "10.3.2", 77 | "@nrwl/nest": "10.3.2", 78 | "@nrwl/node": "10.3.2", 79 | "@nrwl/workspace": "10.3.2", 80 | "@types/jest": "26.0.8", 81 | "@types/node": "~8.9.4", 82 | "codelyzer": "~5.0.1", 83 | "cypress": "^4.1.0", 84 | "dotenv": "6.2.0", 85 | "eslint": "7.10.0", 86 | "jest": "26.2.2", 87 | "jest-preset-angular": "8.3.1", 88 | "prettier": "2.0.4", 89 | "ts-jest": "26.4.0", 90 | "ts-node": "~7.0.0", 91 | "tslint": "~6.0.0", 92 | "typescript": "~4.0.3" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "widgets": [ 3 | { 4 | "id": "1", 5 | "title": "Remote Widget 01", 6 | "description": "Pending..." 7 | }, 8 | { 9 | "id": "2", 10 | "title": "Remote Widget 02", 11 | "description": "Pending..." 12 | }, 13 | { 14 | "id": "3", 15 | "title": "Remote Widget 03", 16 | "description": "Pending..." 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/slides.pdf -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onehungrymind/fem-production-angular/2e938bdea84c2163f8ce046225b031a3311e8c5a/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2017", "dom"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@env/*": ["apps/dashboard/src/environments/*"], 20 | "@fem/api-interfaces": ["libs/api-interfaces/src/index.ts"], 21 | "@fem/core-data": ["libs/core-data/src/index.ts"], 22 | "@fem/core-state": ["libs/core-state/src/index.ts"], 23 | "@fem/material": ["libs/material/src/index.ts"], 24 | "@fem/ui-toolbar": ["libs/ui-toolbar/src/index.ts"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/@nrwl/workspace/src/tslint", 4 | "node_modules/codelyzer" 5 | ], 6 | "linterOptions": { 7 | "exclude": ["**/*"] 8 | }, 9 | "rules": { 10 | "arrow-return-shorthand": true, 11 | "callable-types": true, 12 | "class-name": true, 13 | "deprecation": { 14 | "severity": "warn" 15 | }, 16 | "forin": true, 17 | "import-blacklist": [true, "rxjs/Rx"], 18 | "interface-over-type-literal": true, 19 | "member-access": false, 20 | "member-ordering": [ 21 | true, 22 | { 23 | "order": [ 24 | "static-field", 25 | "instance-field", 26 | "static-method", 27 | "instance-method" 28 | ] 29 | } 30 | ], 31 | "no-arg": true, 32 | "no-bitwise": true, 33 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 34 | "no-construct": true, 35 | "no-debugger": true, 36 | "no-duplicate-super": true, 37 | "no-empty": false, 38 | "no-empty-interface": true, 39 | "no-eval": true, 40 | "no-inferrable-types": [true, "ignore-params"], 41 | "no-misused-new": true, 42 | "no-non-null-assertion": true, 43 | "no-shadowed-variable": true, 44 | "no-string-literal": false, 45 | "no-string-throw": true, 46 | "no-switch-case-fall-through": true, 47 | "no-unnecessary-initializer": true, 48 | "no-unused-expression": true, 49 | "no-var-keyword": true, 50 | "object-literal-sort-keys": false, 51 | "prefer-const": true, 52 | "radix": true, 53 | "triple-equals": [true, "allow-null-check"], 54 | "unified-signatures": true, 55 | "variable-name": false, 56 | "nx-enforce-module-boundaries": [ 57 | true, 58 | { 59 | "enforceBuildableLibDependency": true, 60 | "allow": [], 61 | "depConstraints": [ 62 | { 63 | "sourceTag": "*", 64 | "onlyDependOnLibsWithTags": ["*"] 65 | } 66 | ] 67 | } 68 | ], 69 | "directive-selector": [true, "attribute", "app", "camelCase"], 70 | "component-selector": [true, "element", "app", "kebab-case"], 71 | "no-conflicting-lifecycle": true, 72 | "no-host-metadata-property": true, 73 | "no-input-rename": true, 74 | "no-inputs-metadata-property": true, 75 | "no-output-native": true, 76 | "no-output-on-prefix": true, 77 | "no-output-rename": true, 78 | "no-outputs-metadata-property": true, 79 | "template-banana-in-box": true, 80 | "template-no-negated-async": true, 81 | "use-lifecycle-interface": true, 82 | "use-pipe-transform-interface": true 83 | } 84 | } 85 | --------------------------------------------------------------------------------