├── part-1-environment-setup.md ├── part-2-angular-fundamentals.md ├── book.json ├── assets ├── logos.png ├── vscode.png ├── material-site.png ├── redux-dev-tools.png ├── 2016-11-09_17-02-23.png ├── 2017-07-25_21-00-24.jpg ├── default-ngrx-state.png ├── workspaces-demoapp.png └── angularessentials-img.jpg ├── part-8-angular-services.md ├── part-4-.md ├── .gitbook └── assets │ └── image.png ├── SUMMARY.md ├── .gitignore ├── README.md ├── introduction.md ├── resource-links.md ├── package.json ├── part-15-ci-and-deployment.md ├── chapter1.md ├── part-14-unit-and-e2e-tests.md ├── part-0-environment-setup.md ├── part-1-.md ├── course-description.md ├── part-6-reactive-forms.md ├── part-11-adding-nx-workspaces.md ├── part-5-thirdparty-dependencies.md ├── part-3-generating-components.md ├── part-10-ngrx-selectors.md ├── part-3-angular-services.md ├── part-13-router-actions-and-effects.md ├── part-9-nx-state-libs.md ├── part-7-routing-and-interceptors.md └── part-12-entity-state-adapter.md /part-1-environment-setup.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /part-2-angular-fundamentals.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["highlight", "copy-code-button"] 3 | } -------------------------------------------------------------------------------- /assets/logos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/logos.png -------------------------------------------------------------------------------- /assets/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/vscode.png -------------------------------------------------------------------------------- /part-8-angular-services.md: -------------------------------------------------------------------------------- 1 | # Part 8 - ngrx introduction 2 | 3 | #### 1. Presentation 4 | 5 | Presentation: ngrx 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /part-4-.md: -------------------------------------------------------------------------------- 1 | # Part 3 - RxJS and Angular's HTTP Module 2 | 3 | #### 1. Presentation 4 | 5 | Presentation: RxJS and Observables 6 | 7 | -------------------------------------------------------------------------------- /.gitbook/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/.gitbook/assets/image.png -------------------------------------------------------------------------------- /assets/material-site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/material-site.png -------------------------------------------------------------------------------- /assets/redux-dev-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/redux-dev-tools.png -------------------------------------------------------------------------------- /assets/2016-11-09_17-02-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/2016-11-09_17-02-23.png -------------------------------------------------------------------------------- /assets/2017-07-25_21-00-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/2017-07-25_21-00-24.jpg -------------------------------------------------------------------------------- /assets/default-ngrx-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/default-ngrx-state.png -------------------------------------------------------------------------------- /assets/workspaces-demoapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/workspaces-demoapp.png -------------------------------------------------------------------------------- /assets/angularessentials-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/HEAD/assets/angularessentials-img.jpg -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | * [Introduction](introduction.md) 5 | * Resources 6 | * [Course Description](course-description.md) 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Getting Super Powers 4 | 5 | Becoming a super hero is a fairly straight forward process: 6 | 7 | ``` 8 | $ give me super-powers 9 | ``` 10 | 11 | {% hint style="info" %} 12 | Super-powers are granted randomly so please submit an issue if you're not happy with yours. 13 | {% endhint %} 14 | 15 | Once you're strong enough, save the world: 16 | 17 | ``` 18 | // Ain't no code for that yet, sorry 19 | echo 'You got to trust me on this, I saved the world' 20 | ``` 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Getting Super Powers 4 | 5 | Becoming a super hero is a fairly straight forward process: 6 | 7 | ``` 8 | $ give me super-powers 9 | ``` 10 | 11 | {% hint style="info" %} 12 | Super-powers are granted randomly so please submit an issue if you're not happy with yours. 13 | {% endhint %} 14 | 15 | Once you're strong enough, save the world: 16 | 17 | ``` 18 | // Ain't no code for that yet, sorry 19 | echo 'You got to trust me on this, I saved the world' 20 | ``` 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resource-links.md: -------------------------------------------------------------------------------- 1 | # Resource 2 | 3 | nx documentation - [https://nrwl.io/nx](https://nrwl.io/nx) 4 | 5 | nx playbook - [https://angularplaybook.com/](https://angularplaybook.com/) 6 | 7 | Duncan Hunters NDC London Sharing Code Presentation Slides - [https://docs.google.com/presentation/d/1nk2aPe2aSWL618Q8d1i0biiofRvPh55nqvJDhEmjNaA/edit?usp=sharing](https://docs.google.com/presentation/d/1nk2aPe2aSWL618Q8d1i0biiofRvPh55nqvJDhEmjNaA/edit?usp=sharing) 8 | 9 | ngrx platform example app - [https://github.com/ngrx/platform/blob/master/example-app/README.md](https://github.com/ngrx/platform/blob/master/example-app/README.md) 10 | 11 | angular.io - [https://angular.io](https://angular.io) 12 | 13 | cli.angular.io - [https://cli.angular.io](https://cli.angular.io) 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dunchunter/enterprise-angular-applications-with-ngrx-and-nx", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx/issues" 17 | }, 18 | "homepage": "https://github.com/duncanhunter/Enterprise-Angular-applications-with-ngrx-and-nx#readme", 19 | "dependencies": { 20 | "gitbook-plugin-copy-code-button": "0.0.2", 21 | "gitbook-plugin-highlight": "^2.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /part-15-ci-and-deployment.md: -------------------------------------------------------------------------------- 1 | # Part 15 Deploying your nx monorepo 2 | 3 | The AngularCLI only generates bundles. This means that we cannot build the lib itself. We can only do it by building an app that depends on it. 4 | 5 | And then run 6 | 7 | ``` 8 | ng build --prod -a=admin-portal 9 | ``` 10 | 11 | In this section, we will run some analysis tools to inspect the size of our application. 12 | 13 | * Install the [webpack-bundle-analyzer](https://github.com/th0r/webpack-bundle-analyzer) 14 | 15 | ``` 16 | npm install --save-dev webpack-bundle-analyzer 17 | ``` 18 | 19 | * Once installed add the following entry to the npm scripts in the package.json: 20 | 21 | ``` 22 | "bundle-report-admin-portal": "webpack-bundle-analyzer dist/apps/admin-portal/stats.json" 23 | ``` 24 | 25 | * Rebuild with --stats-json 26 | 27 | ``` 28 | ng build --prod -a=admin-portal --stats-json 29 | ``` 30 | 31 | * Run the following command 32 | 33 | ``` 34 | npm run bundle-report 35 | ``` 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /chapter1.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 1. Creating an nx workspace 4 | 2. Generating components 5 | 3. Adding Services 6 | 4. RxJS and Angulars HTTP Module 7 | 5. Thirdparty dependencies 8 | 6. Reactive Forms 9 | 7. Routing and interceptors 10 | 8. ngrx introduction 11 | 9. nx state libs 12 | 10. ngrx selectors 13 | 11. Adding nx workspaces 14 | 12. Entity state adapter 15 | 13. Router actions and effects 16 | 14. Unit and e2e testing 17 | 15. Deploying your nx monorepo 18 | 19 | ### Finished Workshop Code {#finishedcodeforthecrmmodule} 20 | 21 | ``` 22 | git clone "https://github.com/duncanhunter/Demo-App-Enterprise-Angular-applications-with-ngrx-and-nx.git" 23 | 24 | cd demo-app 25 | 26 | npm install 27 | ``` 28 | 29 | ### WIP Workshop Code {#wipcoursecode} 30 | 31 | ``` 32 | git clone "https://github.com/duncanhunter/Demo-App-NDC-London-2018-Enterprise-Angular-applications-with-ngrx-and-nx” 33 | 34 | cd demo-app 35 | 36 | npm install 37 | ``` 38 | 39 | ### Course Slides {#courseslides} 40 | 41 | [https://docs.google.com/presentation/d/1MknPww1MdwzreFvDh086TzqaB5yyQga0PaoRR9bgCXs/edit?usp=sharing](https://docs.google.com/presentation/d/1MknPww1MdwzreFvDh086TzqaB5yyQga0PaoRR9bgCXs/edit?usp=sharing) 42 | 43 | -------------------------------------------------------------------------------- /part-14-unit-and-e2e-tests.md: -------------------------------------------------------------------------------- 1 | # Part 14 - Unit and e2e tests 2 | 3 | 4 | 5 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 6 | 7 | import { LoginFormComponent } from './login-form.component'; 8 | import { MaterialModule } from '@demo-app/material'; 9 | import { ReactiveFormsModule } from '@angular/forms'; 10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 11 | import { Authenticate } from '@demo-app/data-models'; 12 | 13 | fdescribe('LoginFormComponent', () => { 14 | let component: LoginFormComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach( 18 | async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [MaterialModule, ReactiveFormsModule, BrowserAnimationsModule], 21 | declarations: [LoginFormComponent] 22 | }).compileComponents(); 23 | }) 24 | ); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(LoginFormComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | 36 | it(`should have a disabled form on start`, () => { 37 | expect(component.loginForm.invalid).toEqual(true); 38 | }); 39 | 40 | it(`should be valid with authenticate object`, () => { 41 | const authenticate: Authenticate = {username: 'duncan', password: '123'}; 42 | component.loginForm.setValue(authenticate); 43 | expect(component.loginForm.valid).toEqual(true); 44 | }); 45 | }); 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /part-0-environment-setup.md: -------------------------------------------------------------------------------- 1 | # Part 0 - Environment setup 2 | 3 | Dependency checklist: 4 | 5 | 1. node v6+ 6 | 2. git 7 | 3. cli.angular.io 8 | 4. nrwl schematics 9 | 5. visual studio code 10 | 6. visual studio code extensions 11 | 7. chrome extension for redux dev tools 12 | 13 | The main dependency for being able to make an Angular application is node version 6+. The latest stable version of node is best to get if you do not have it already installed. 14 | 15 | ### Check you have the right node and git 16 | 17 | You can check your version of node by running the following command in the terminal. 18 | 19 | ``` 20 | node -v 21 | ``` 22 | 23 | If you would like to use source control and check out completed work then it is recommended to have git installed on your machine. You can download git from[ https://git-scm.com/downloads ](https://git-scm.com/downloads ) 24 | 25 | You can check your version of node by running the following command in the terminal. 26 | 27 | ``` 28 | git --version 29 | ``` 30 | 31 | If you do not have node installed or you are using a version lower than v4 then I you can get the latest stable version from [www.nodejs.org](/www.nodejs.org). 32 | 33 | ### Install Angular CLI and NX 34 | 35 | We need to have both the Angular CLI and the nrwl schematics installed globally. Run the following commands. 36 | 37 | ``` 38 | npm install -g @angular/cli 39 | ``` 40 | 41 | ``` 42 | npm install -g @nrwl/schematics 43 | ``` 44 | 45 | ### **Get Visual Studio Code ** 46 | 47 | [**https://code.visualstudio.com/**](https://code.visualstudio.com/) 48 | 49 | ![](/assets/vscode.png) 50 | 51 | ### Get **Visual Studio Code ** Extensions![](/assets/2016-11-09_17-02-23.png)![](/assets/angularessentials-img.jpg) 52 | 53 | ### Optionally turn on **Visual Studio Code autosave** 54 | 55 | ![](/assets/2017-07-25_21-00-24.jpg) 56 | 57 | #### 7. Install redux dev tools chrome extension 58 | 59 | [https://github.com/zalmoxisus/redux-devtools-extension](https://github.com/zalmoxisus/redux-devtools-extension) 60 | 61 | ![](/assets/redux-dev-tools.png) 62 | 63 | -------------------------------------------------------------------------------- /part-1-.md: -------------------------------------------------------------------------------- 1 | # Part 1 - Creating an nx workspace 2 | 3 | We will be building out the beginning of two applications a customer portal and an admin portal. 4 | 5 | ![](/assets/workspaces-demoapp.png)_**figure: nx workspaces diagram**_ 6 | 7 | #### 1 .Create a new nx workspace 8 | 9 | * Run the below command in a terminal to make a new nx workspace. 10 | 11 | ``` 12 | create-nx-workspace demo-app 13 | ``` 14 | 15 | #### 2. Examine the output of the following files and commit code to git source control 16 | 17 | 1. Run the following command to open the new nx workspace in VSCode. 18 | 19 | ``` 20 | cd demo-app && code . 21 | ``` 22 | 23 | Until this pull request [https://github.com/nrwl/nx/issues/198](https://github.com/nrwl/nx/issues/198) is merged run the following in the terminal to avoid warnings in terminal. 24 | 25 | ``` 26 | npm install prettier@1.10.1 --save 27 | ``` 28 | 29 | * Inspect the following files: 30 | 31 | * .angular-cli.json 32 | 33 | * tsconfig paths 34 | 35 | * package.json 36 | 37 | #### 3. Change from css to scss 38 | 39 | * Open up angular .angular-cli.json and change the default style type. 40 | 41 | _**.angular-cli.json**_ 42 | 43 | ``` 44 | "styleExt": "scss" 45 | ``` 46 | 47 | * Commit the initial NX workspace to source control 48 | 49 | #### 4. Create a new workspace app 50 | 51 | * By default a new NX workspace has no apps or libs yet. You can run the below command to see the extra options to make an app or a lib besides the normal angular CLI commands. 52 | 53 | ``` 54 | ng g app --help 55 | ``` 56 | 57 | * Create a new app by running the below command in the terminal in a directory of your choice. 58 | 59 | ``` 60 | ng g app customer-portal --style scss --routing 61 | ``` 62 | 63 | #### 5. Add ngrx 64 | 65 | * Add a default set up for ngrx to our new app. We can run the generate command for ngrx with the module and --onlyEmptyRoot option to only add the StoreModule.forRoot and EffectsModule.forRoot calls without generating any new files versus --root which will make a default reducer and effect. 66 | 67 | ``` 68 | ng g ngrx app --module=apps/customer-portal/src/app/app.module.ts --onlyEmptyRoot 69 | ``` 70 | 71 | #### 6. Examine angular app and module structure 72 | 73 | 1. app.module.ts 74 | 2. tsconfig paths 75 | 3. package.json 76 | 4. apps and libs empty 77 | 78 | #### 7. Run the app in the browser 79 | 80 | * Run the following command to launch the app in the browser. -a is for the app to start and -o is to open in the default browser. 81 | 82 | ``` 83 | ng s -a=customer-portal -o 84 | ``` 85 | 86 | * See the default state of the app in the redux dev tools![](/assets/default-ngrx-state.png) 87 | 88 | #### 8. Commit to repo for the class \(Instructor only\) 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /course-description.md: -------------------------------------------------------------------------------- 1 | # Course Description 2 | 3 | ## Workshop: Enterprise Angular applications with ngrx and nx 4 | 5 | ![](.gitbook/assets/image.png) 6 | 7 | Building enterprise Angular applications is not easy and when you need to share code between multiple Angular mobile or web applications it is even harder. If you are about to kick off or are in the middle of a large application in Angular this workshop will prepare you to tackle implementing the best project structure and patterns using nx workspaces and ngrx state management patterns. 8 | 9 | In this workshop we walk through a thorough introduction into using ngrx, an RxJS powered state management library for Angular applications, inspired by Redux. We will use nx or Nrwl Extensions an open source toolkit for enterprise Angular applications. nx is designed to help you create and build enterprise-grade Angular applications with proven project structure and patterns. 10 | 11 | You will learn to use nx to create a monorepo creating one or many Angular applications with a robust code sharing system using nx workspaces and shared libraries. 12 | 13 | We will build and test an nx application implementing ngrx actions, reducers, effects, entity adapters, router-store, onPush change detection and a single immutable data structure called the store. 14 | 15 | We will look at common patterns for structuring your applications state by feature and how to deal with splitting up related data into multiple reducers. Then will we will look at how to create selectors to combine multiple slices of state from the store. 16 | 17 | By the end of this workshop you will have built a working nx and ngrx application you can extend into an enterprise application. You will also walk away with the source code and the course material. So join me and bring your laptops to this workshop where you will get to code along as we build and learn to make awesome Angular applications. 18 | 19 | **Day 1** 20 | 21 | * Angular mono repo architecture with the CLI and nx workspaces 22 | * Building shared nx libraries with ngrx 23 | * Redux and nrgx principles 24 | * Angular fundamentals including components, routing, services and forms 25 | 26 | **Day 2** 27 | 28 | * Entity State adapter 29 | * Splitting up related data into multiple reducers 30 | * Creating ngrx selectors 31 | * Router actions and effects 32 | * Building and deploying your nx application 33 | * Unit and e2e testing 34 | 35 | **Prerequisites** This workshop is for developers with at least a basic understanding of JavaScript and HTML. You do not need angular v2+ experience to attend this course but it is recommended to have done at least the beginner's tutorial on angular.io or equivalent. This course briefly covers the fundamentals of angular components, services, routing and modules but moves onto talking about using them with ngrx and nx workspaces and enterprise patterns for the majority of the workshop. 36 | 37 | **Computer Setup:** You need to bring your own laptop with the below software installed to follow this workshop. 38 | 39 | * Visual Studio Code 40 | * A command line Git client 41 | * NodeJS \(Latest stable version\) 42 | * Angular CLI 43 | 44 | If you get stuck we can help you on the day but it helps to have this already installed. 45 | 46 | -------------------------------------------------------------------------------- /part-6-reactive-forms.md: -------------------------------------------------------------------------------- 1 | # Part 6 - Reactive Forms 2 | 3 | #### 1.Add ngx-errors library to make it easier to display form errors 4 | 5 | [https://github.com/UltimateAngular/ngx-errors](https://github.com/UltimateAngular/ngx-errors) 6 | 7 | ``` 8 | npm i @ultimate/ngxerrors 9 | ``` 10 | 11 | 1. Add ravtice forms module to auth module imports 12 | 13 | ```ts 14 | imports: [CommonModule, RouterModule, HttpClientModule, MaterialModule, ReactiveFormsModule], 15 | ``` 16 | 17 | #### 2. Add a reactive FormGroup to login form 18 | 19 | Note: To save injecting the formBuilder and keeping this a presentational component with no injected dependancies we can just new up a simple FormGroup. You can read more about it here[ https://angular.io/api/forms/FormBuilder](https://angular.io/api/forms/FormBuilder). 20 | 21 | _**libs/auth/src/components/login-form/login-form.component.ts**_ 22 | 23 | ```ts 24 | import { Component, OnInit, EventEmitter, Output } from '@angular/core'; 25 | import { Authenticate } from '@demo-app/data-models'; 26 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 27 | 28 | @Component({ 29 | selector: 'login-form', 30 | templateUrl: './login-form.component.html', 31 | styleUrls: ['./login-form.component.scss'] 32 | }) 33 | export class LoginFormComponent { 34 | @Output() submit = new EventEmitter(); 35 | 36 | loginForm = new FormGroup({ 37 | username: new FormControl('', [Validators.required]), 38 | password: new FormControl('', [Validators.required]) 39 | }); 40 | 41 | login() { 42 | this.submit.emit({ 43 | username: this.loginForm.value.username, 44 | password: this.loginForm.value.password 45 | }); 46 | } 47 | } 48 | ``` 49 | 50 | * Add ngx-errors to the form 51 | 52 | _**libs/auth/src/components/login-form/login-form.component.html**_ 53 | 54 | ```html 55 | 56 | Login 57 | 58 |
59 | 60 | 61 | 62 |
63 | Username is required 64 |
65 |
66 |
67 | 68 | 69 | 70 |
71 | Password is required 72 |
73 |
74 |
75 |
76 | 77 |
78 |
79 | ``` 80 | 81 | _**libs/data-models/src/data-models.ts**_ 82 | 83 | ```ts 84 | export interface User { 85 | username: string; 86 | id: number; 87 | country: string; 88 | token: string 89 | role: string; 90 | } 91 | ``` 92 | 93 | _**libs/data-models/index.ts**_ 94 | 95 | ```ts 96 | export { Authenticate, User } from './src/data-models'; 97 | ``` 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /part-11-adding-nx-workspaces.md: -------------------------------------------------------------------------------- 1 | # Part 11 - Adding more nx workspaces 2 | 3 | #### 1. Add another app to our nx workspace called admin-portal 4 | 5 | ``` 6 | ng g app admin-portal --style scss --routing 7 | ``` 8 | 9 | * add default ngrx setup to our new app 10 | 11 | ``` 12 | ng g ngrx app --module=apps/admin-portal/src/app/app.module.ts --onlyEmptyRoot 13 | ``` 14 | 15 | #### 2. Add scripts to start each app more easily to package.json 16 | 17 | _**package.json**_ 18 | 19 | ```json 20 | "scripts": { 21 | ... 22 | "customer-portal": "ng serve -a=customer-portal -p=4200", 23 | "admin-portal": "ng serve -a=admin-portal -p=4201", 24 | ... 25 | } 26 | ``` 27 | 28 | * Add auth module to the new app 29 | 30 | ```ts 31 | @NgModule({ 32 | imports: [ 33 | ... 34 | AuthModule.forRoot() 35 | ... 36 | ], 37 | declarations: [AppComponent], 38 | bootstrap: [AppComponent], 39 | providers: [AppEffects] 40 | }) 41 | export class AppModule { } 42 | ``` 43 | 44 | #### 3. Add a new layout lib and component 45 | 46 | ``` 47 | ng g lib layout --directory=admin-portal 48 | ``` 49 | 50 | * Add a layout container component 51 | 52 | ``` 53 | ng g c containers/layout -a=admin-portal/layout 54 | ``` 55 | 56 | * Add a material tool bar 57 | 58 | _**libs/admin-portal/layout/src/containers/layout/layout.component.html**_ 59 | 60 | ```html 61 | 62 | Admin Portal 63 |
64 | {{(user$ | async)?.username}} 65 |
66 |
67 | 68 | ``` 69 | 70 | * Add the new component to the admin-portal apps main view 71 | 72 | _**apps/admin-portal/src/app/app.component.html**_ 73 | 74 | ```html 75 | 76 | 77 | 78 | ``` 79 | 80 | * Add styles to styles.scss 81 | 82 | _**apps/admin-portal/src/styles.scss**_ 83 | 84 | ```css 85 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 86 | 87 | body { 88 | margin: 0; 89 | } 90 | ``` 91 | 92 | * Do not forget the browser animations module for Materials dependency and the basic routes 93 | 94 | NOTE: When we re-use a module we need to manually configure the routing 95 | 96 | _**apps/admin-portal/src/app/app.module.ts**_ 97 | 98 | ```ts 99 | @NgModule({ 100 | imports: [ 101 | BrowserModule, 102 | NxModule.forRoot(), 103 | RouterModule.forRoot( 104 | [ 105 | { path: '', pathMatch: 'full', redirectTo: 'user-profile' }, 106 | { path: 'auth', children: authRoutes }, 107 | { 108 | path: 'user-profile', 109 | loadChildren: '@demo-app/user-profile#UserProfileModule', 110 | canActivate: [AuthGuard] 111 | } 112 | ]), 113 | BrowserAnimationsModule, 114 | ... 115 | ``` 116 | 117 | * As we did not generate the Auth module to sync with the new app we need to manually register the lazy loaded parts 118 | 119 | _**apps/admin-portal/src/tsconfig.app.json**_ 120 | 121 | ```ts 122 | { 123 | "extends": "../../../tsconfig.json", 124 | "compilerOptions": { 125 | "outDir": "../../../dist/out-tsc/apps/admin-portal", 126 | "module": "es2015" 127 | }, 128 | "include": [ 129 | "**/*.ts" 130 | /* add all lazy-loaded libraries here: "../../../libs/my-lib/index.ts" */ 131 | , "../../../libs/user-profile/index.ts" 132 | ], 133 | "exclude": [ 134 | "**/*.spec.ts" 135 | ] 136 | } 137 | ``` 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /part-5-thirdparty-dependencies.md: -------------------------------------------------------------------------------- 1 | # Part 5 - Thirdparty dependencies 2 | 3 | ![](/assets/material-site.png) 4 | 5 | [https://material.angular.io/](https://material.angular.io/) 6 | 7 | #### 1.Install angular material, angular animations and angular flex layout 8 | 9 | ``` 10 | npm install --save @angular/material @angular/cdk @angular/flex-layout @angular/animations 11 | ``` 12 | 13 | * Add animations module to the main app module. 14 | 15 | ```ts 16 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 17 | 18 | @NgModule({ 19 | ... 20 | imports: [BrowserAnimationsModule], 21 | ... 22 | }) 23 | export class AppModule { } 24 | ``` 25 | 26 | #### 2.Add a new nx lib to hold all the common material components we will use in our app 27 | 28 | ``` 29 | ng g lib material 30 | ``` 31 | 32 | * Add all the common material components and re-export them 33 | 34 | _**libs/material/src/material.module.ts**_ 35 | 36 | ```ts 37 | import { NgModule } from '@angular/core'; 38 | import { FlexLayoutModule } from '@angular/flex-layout'; 39 | 40 | import { 41 | MatInputModule, 42 | MatCardModule, 43 | MatButtonModule, 44 | MatSidenavModule, 45 | MatListModule, 46 | MatIconModule, 47 | MatToolbarModule, 48 | MatProgressSpinnerModule, 49 | MatMenuModule, 50 | MatTableModule, 51 | MatSelectModule 52 | } from '@angular/material'; 53 | 54 | @NgModule({ 55 | imports: [ 56 | FlexLayoutModule, 57 | MatInputModule, 58 | MatCardModule, 59 | MatButtonModule, 60 | MatSidenavModule, 61 | MatListModule, 62 | MatIconModule, 63 | MatToolbarModule, 64 | MatProgressSpinnerModule, 65 | MatMenuModule, 66 | MatTableModule, 67 | MatSelectModule 68 | ], 69 | exports: [ 70 | FlexLayoutModule, 71 | MatInputModule, 72 | MatCardModule, 73 | MatButtonModule, 74 | MatSidenavModule, 75 | MatListModule, 76 | MatIconModule, 77 | MatToolbarModule, 78 | MatProgressSpinnerModule, 79 | MatMenuModule, 80 | MatTableModule, 81 | MatSelectModule 82 | ] 83 | }) 84 | export class MaterialModule {} 85 | 86 | ``` 87 | 88 | * Add material module to auth module 89 | 90 | _**libs/auth/src/auth.module.ts**_ 91 | 92 | ```ts 93 | import { MaterialModule } from '@demo-app/material'; 94 | 95 | @NgModule({ 96 | ... 97 | imports: [CommonModule, RouterModule, HttpClientModule, MaterialModule], 98 | ... 99 | }) 100 | export class AuthModule { } 101 | ``` 102 | 103 | #### 3. Add material default styles 104 | 105 | _**apps/customer-portal/src/styles.scss**_ 106 | 107 | ```css 108 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 109 | ``` 110 | 111 | * Update the login-form to use material components 112 | 113 | _**libs/auth/src/components/login-form/login-form.component.html**_ 114 | 115 | ```html 116 | 117 | Login 118 | 119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 | 128 |
129 |
130 | ``` 131 | 132 | #### 4. Go and explore flex layout docs 133 | 134 | [https://tburleson-layouts-demos.firebaseapp.com/\#/docs](https://tburleson-layouts-demos.firebaseapp.com/#/docs) 135 | 136 | -------------------------------------------------------------------------------- /part-3-generating-components.md: -------------------------------------------------------------------------------- 1 | # Part 2 - Generating components and nx libs 2 | 3 | #### 1. Generate a lib 4 | 5 | * Run the below command to see all the lib options 6 | 7 | ``` 8 | ng g lib --help 9 | ``` 10 | 11 | * Add a new lib called auth. We will not lazy load this lib as auth will always be used by our app 12 | 13 | ``` 14 | ng g lib auth --routing --parent-module=apps/customer-portal/src/app/app.module.ts 15 | ``` 16 | 17 | #### 2. Add container and presentational components 18 | 19 | * Add a new container component to the auth lib 20 | 21 | ``` 22 | ng g c containers/login -a=auth 23 | ``` 24 | 25 | * Add a new presentational component to the auth lib 26 | 27 | ``` 28 | ng g c components/login-form -a=auth 29 | ``` 30 | 31 | You may decide not to make this a presentational component but it makes it easier to test and refactor 32 | 33 | * Add a route to the auth module and a custom components array to make it easier to re-export components. 34 | 35 | _**libs/auth/src/auth.module.ts**_ 36 | 37 | ```ts 38 | import { NgModule } from '@angular/core'; 39 | import { CommonModule } from '@angular/common'; 40 | import { RouterModule, Route } from '@angular/router'; 41 | import { LoginComponent } from './containers/login/login.component'; 42 | import { LoginFormComponent } from './components/login-form/login-form.component'; 43 | 44 | export const authRoutes: Route[] = [{ path: 'login', component: LoginComponent }]; 45 | const COMPONENTS = [LoginComponent, LoginFormComponent]; 46 | 47 | @NgModule({ 48 | imports: [CommonModule, RouterModule], 49 | declarations: [COMPONENTS], 50 | exports: [COMPONENTS] 51 | }) 52 | export class AuthModule {} 53 | ``` 54 | 55 | * Inspect the index.ts file which is for exporting public api surface for the lib. 56 | 57 | * Inspect .angular-cli.json libs array allow us to use -a with libs to get cli scaffolding and code generation 58 | 59 | * Delete everything but the router-outlet on the apps app.component.html file 60 | 61 | _**apps/customer-portal/src/app.components.ts**_ 62 | 63 | ```ts 64 | 65 | ``` 66 | 67 | * Add the presentational component to the container component. 68 | 69 | _**libs/auth/src/containers/login/login.component.html**_ 70 | 71 | ```html 72 | 73 | ``` 74 | 75 | * Add login function to container component class 76 | 77 | _**libs/auth/src/containers/login/login.component.ts**_ 78 | 79 | ```ts 80 | import { Component, OnInit } from '@angular/core'; 81 | 82 | @Component({ 83 | selector: 'app-login', 84 | templateUrl: './login.component.html', 85 | styleUrls: ['./login.component.scss'] 86 | }) 87 | export class LoginComponent implements OnInit { 88 | constructor() {} 89 | 90 | ngOnInit() {} 91 | 92 | login(authenticate:any) { 93 | console.log(authenticate); 94 | } 95 | } 96 | ``` 97 | 98 | #### 3. Add new nx lib for data models 99 | 100 | * Make another lib but this time with a --nomodule flag as we just want to export our files not register a module with angular. 101 | 102 | ``` 103 | ng generate lib data-models --nomodule 104 | ``` 105 | 106 | * Add the following file to the lib and export the added data models from the index.ts file 107 | 108 | _**libs/data-models/src/data-models.ts**_ 109 | 110 | ```ts 111 | export interface Authenticate { 112 | username: string; 113 | password: string; 114 | } 115 | ``` 116 | 117 | * Export the interface 118 | 119 | _**libs/data-models**_ 120 | 121 | ```ts 122 | export { Authenticate} from './src/data-models' 123 | ``` 124 | 125 | * Add basic login form to presentational component 126 | 127 | _**libs/auth/src/components/login-form/login-form.component.html**_ 128 | 129 | ```html 130 | 131 | 132 | 133 | ``` 134 | 135 | * Add an angular @Output to emit the event of a form submission 136 | 137 | _**libs/auth/src/components/login-form/login-form.component.ts**_ 138 | 139 | ```ts 140 | import { Component, OnInit, EventEmitter, Output } from '@angular/core'; 141 | import { Authenticate } from '@demo-app/data-models'; 142 | 143 | @Component({ 144 | selector: 'login-form', 145 | templateUrl: './login-form.component.html', 146 | styleUrls: ['./login-form.component.scss'] 147 | }) 148 | export class LoginFormComponent { 149 | @Output() submit = new EventEmitter(); 150 | 151 | login(authenticate: Authenticate) { 152 | this.submit.emit(authenticate); 153 | } 154 | } 155 | ``` 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /part-10-ngrx-selectors.md: -------------------------------------------------------------------------------- 1 | # Part 10 - ngrx selectors 2 | 3 | Lets add a main menu to our customer portal to show the name of the logged in user. 4 | 5 | 1. #### Add a new layout lib to a customer-portal directory 6 | 7 | ``` 8 | ng g lib layout --directory=customer-portal 9 | ``` 10 | 11 | #### 2. Add a layout container component 12 | 13 | ``` 14 | ng g c containers/layout -a=customer-portal/layout 15 | ``` 16 | 17 | #### 3. Add Material and Router module 18 | 19 | _**libs/customer-portal/layout/src/layout.module.ts**_ 20 | 21 | ```ts 22 | import { NgModule } from '@angular/core'; 23 | import { CommonModule } from '@angular/common'; 24 | import { MaterialModule } from '@demo-app/material'; 25 | import { LayoutComponent } from './containers/layout/layout.component'; 26 | import { RouterModule } from '@angular/router'; 27 | 28 | const COMPONENTS = [LayoutComponent]; 29 | @NgModule({ 30 | imports: [CommonModule, MaterialModule, RouterModule], 31 | declarations: [COMPONENTS], 32 | exports: [COMPONENTS] 33 | }) 34 | export class LayoutModule { 35 | } 36 | ``` 37 | 38 | #### 3. Add a material toolbar 39 | 40 | _**libs/customer-portal/layout/src/containers/layout/layout.component.html**_ 41 | 42 | ```ts 43 | 44 | Customer Portal 45 |
46 | {{(user$ | async)?.username}} 47 |
48 |
49 | 50 | ``` 51 | 52 | * Add styles to styles.scss 53 | 54 | _**apps/customer-portal/src/styles.scss**_ 55 | 56 | ```css 57 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 58 | 59 | body { 60 | margin: 0; 61 | } 62 | ``` 63 | 64 | _**libs/customer-portal/layout/src/containers/layout/layout.component.scss**_ 65 | 66 | ```css 67 | .right-nav { 68 | margin-left: auto; 69 | } 70 | ``` 71 | 72 | #### 4. Update layout component to select user from the store 73 | 74 | _**libs/customer-portal/layout/src/containers/layout/layout.component.ts**_ 75 | 76 | ```ts 77 | import { Component, OnInit } from '@angular/core'; 78 | import { Store } from '@ngrx/store'; 79 | import { AuthState } from '@demo-app/auth'; 80 | import { User } from '@demo-app/data-models'; 81 | import { Observable } from 'rxjs/Observable'; 82 | 83 | @Component({ 84 | selector: 'app-layout', 85 | templateUrl: './layout.component.html', 86 | styleUrls: ['./layout.component.scss'] 87 | }) 88 | export class LayoutComponent implements OnInit { 89 | user$: Observable; 90 | constructor(private store: Store) { } 91 | 92 | ngOnInit() { 93 | this.user$ = this.store.select(state => state.auth.user); 94 | } 95 | 96 | } 97 | ``` 98 | 99 | * Add layout module to the customer-portal app 100 | 101 | _**apps/customer-portal/src/app/app.module.ts**_ 102 | 103 | ```ts 104 | import { LayoutModule } from '@demo-app/customer-portal/layout'; 105 | 106 | @NgModule({ 107 | imports: [ 108 | ... 109 | LayoutModule 110 | ... 111 | ], 112 | declarations: [AppComponent], 113 | bootstrap: [AppComponent] 114 | }) 115 | export class AppModule {} 116 | ``` 117 | 118 | * Add the layout component to the main app.component.html 119 | 120 | _**apps/customer-portal/src/app/app.component.html**_ 121 | 122 | ```html 123 | 124 | 125 | 126 | ``` 127 | 128 | #### 5. Add selector file 129 | 130 | * Add a file called index.ts to the +state folder of your auth state lib 131 | 132 | _**libs/auth/src/+state/index.ts**_ 133 | 134 | ```ts 135 | import { createSelector, createFeatureSelector } from '@ngrx/store'; 136 | import { Auth } from './auth.interfaces'; 137 | 138 | export const getAuthState = createFeatureSelector('auth'); 139 | export const getUser = createSelector(getAuthState, state => state.user); 140 | ``` 141 | 142 | * Ensure you have re-exported your publically available paths in the auth libs index.ts file 143 | 144 | _**libs/auth/index.ts**_ 145 | 146 | ```ts 147 | export { AuthModule , authRoutes } from './src/auth.module'; 148 | export { AuthGuard } from './src/guards/auth.guard'; 149 | export { AuthState } from './src/+state/auth.interfaces'; 150 | export * from './src/+state'; 151 | ``` 152 | 153 | #### 6. Use selector in Layout component 154 | 155 | _**libs/customer-portal/layout/src/containers/layout/layout.component.ts**_ 156 | 157 | ```ts 158 | import { Component, OnInit } from '@angular/core'; 159 | import { Store } from '@ngrx/store'; 160 | import { AuthState, getUser } from '@demo-app/auth'; 161 | import { User } from '@demo-app/data-models'; 162 | import { Observable } from 'rxjs/Observable'; 163 | 164 | @Component({ 165 | selector: 'app-layout', 166 | templateUrl: './layout.component.html', 167 | styleUrls: ['./layout.component.scss'] 168 | }) 169 | export class LayoutComponent implements OnInit { 170 | user$: Observable; 171 | constructor(private store: Store) { } 172 | 173 | ngOnInit() { 174 | this.user$ = this.store.select(getUser); 175 | } 176 | 177 | } 178 | ``` 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /part-3-angular-services.md: -------------------------------------------------------------------------------- 1 | # Part 4 - Angular Services 2 | 3 | #### 1. Generate a new angular service 4 | 5 | * Run the following command to make a new service in the auth lib 6 | 7 | ``` 8 | ng g service services/auth -a=auth 9 | ``` 10 | 11 | #### 2. Add login method and http post for the login 12 | 13 | _**libs/auth/src/services/auth.service.ts**_ 14 | 15 | ```ts 16 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 17 | import { Injectable } from '@angular/core'; 18 | import { Authenticate } from '@demo-app/data-models'; 19 | 20 | @Injectable() 21 | export class AuthService { 22 | constructor(private httpClient: HttpClient) {} 23 | 24 | login(authenticate:Authenticate) { 25 | return this.httpClient.post('http://localhost:3000/login', authenticate); 26 | } 27 | } 28 | ``` 29 | 30 | * Export the auth service from the auth lib and add a static for root method. 31 | 32 | _**libs/auth/src/auth.module.ts**_ 33 | 34 | ```ts 35 | import { NgModule, ModuleWithProviders } from '@angular/core'; 36 | import { CommonModule } from '@angular/common'; 37 | import { RouterModule, Route } from '@angular/router'; 38 | import { LoginComponent } from './containers/login/login.component'; 39 | import { HttpClientModule } from '@angular/common/http'; 40 | import { AuthService } from './services/auth.service'; 41 | 42 | export const authRoutes: Route[] = [ 43 | { path: 'login', component: LoginComponent } 44 | ]; 45 | const COMPONENTS = [LoginComponent]; 46 | 47 | @NgModule({ 48 | imports: [CommonModule, RouterModule, HttpClientModule], 49 | declarations: [COMPONENTS], 50 | exports: [COMPONENTS], 51 | providers: [AuthService] 52 | }) 53 | export class AuthModule { 54 | static forRoot(): ModuleWithProviders { 55 | return { 56 | ngModule: AuthModule, 57 | providers: [AuthService] 58 | }; 59 | } 60 | } 61 | ``` 62 | 63 | * update the app module to import the auth lib 64 | 65 | _**apps/customer-portal/src/app/app.module.ts**_ 66 | 67 | ```ts 68 | AuthModule.forRoot() 69 | ``` 70 | 71 | #### 3. Update login component to call the service 72 | 73 | ```ts 74 | import { Component, OnInit } from '@angular/core'; 75 | import { AuthService } from './../../services/auth.service'; 76 | import { Authenticate } from '@demo-app/data-models'; 77 | 78 | @Component({ 79 | selector: 'app-login', 80 | templateUrl: './login.component.html', 81 | styleUrls: ['./login.component.scss'] 82 | }) 83 | export class LoginComponent implements OnInit { 84 | constructor(private authService: AuthService) {} 85 | 86 | ngOnInit() {} 87 | 88 | login(authenticate: Authenticate) { 89 | this.authService.login(authenticate).subscribe() 90 | } 91 | } 92 | ``` 93 | 94 | #### 4. Add a json-server to be able to make http requests and mock a real server 95 | 96 | * Add a folder called server to the root directory 97 | 98 | * Add a file called db.json and server.ts to the new server folder 99 | 100 | * add json-server and ts-node 101 | 102 | ``` 103 | npm i json-server ts-node --save-dev 104 | ``` 105 | 106 | * Add the below script to the scripts in the package.json 107 | 108 | _**package.json**_ 109 | 110 | ```json 111 | scripts: { 112 | ... 113 | "server": "ts-node ./server/server.ts" 114 | ... 115 | } 116 | ``` 117 | 118 | * Add the default mock data to the db.json file 119 | 120 | _**server/db.json**_ 121 | 122 | ```json 123 | { 124 | "users": [ 125 | { 126 | "id": 1, 127 | "username": "duncan", 128 | "country": "australia", 129 | "password": "123" 130 | }, 131 | { 132 | "id": 2, 133 | "username": "sarah", 134 | "country": "england", 135 | "password": "123" 136 | }, 137 | { 138 | "id": 3, 139 | "username": "admin", 140 | "country": "usa", 141 | "password": "123" 142 | }, 143 | { 144 | "username": "test2", 145 | "password": "123", 146 | "id": 4 147 | } 148 | ], 149 | "profiles": [ 150 | { 151 | "id": 1, 152 | "userId": 1, 153 | "job": "plumber" 154 | }, 155 | { 156 | "id": 2, 157 | "userId": 2, 158 | "job": "developer" 159 | }, 160 | { 161 | "id": 3, 162 | "userId": 3, 163 | "job": "manager" 164 | } 165 | ] 166 | } 167 | ``` 168 | 169 | * Add the mock server code below 170 | 171 | _**server/server.ts**_ 172 | 173 | ```ts 174 | const jsonServer = require('json-server'); 175 | const server = jsonServer.create(); 176 | const router = jsonServer.router('server/db.json'); 177 | const middlewares = jsonServer.defaults(); 178 | const db = require('./db.json'); 179 | const fs = require('fs'); 180 | 181 | server.use(middlewares); 182 | server.use(jsonServer.bodyParser); 183 | 184 | server.post('/login', (req, res, next) => { 185 | const users = readUsers(); 186 | 187 | const user = users.filter( 188 | u => u.username === req.body.username && u.password === req.body.password 189 | )[0]; 190 | 191 | if (user) { 192 | res.send({ ...formatUser(user), token: checkIfAdmin(user) }); 193 | } else { 194 | res.status(400).send('Incorrect username or password'); 195 | } 196 | }); 197 | 198 | server.post('/register', (req, res) => { 199 | const users = readUsers(); 200 | const user = users.filter(u => u.username === req.body.username)[0]; 201 | 202 | if (user === undefined || user === null) { 203 | res.send({ 204 | ...formatUser(req.body), 205 | token: checkIfAdmin(req.body) 206 | }); 207 | db.users.push(req.body); 208 | } else { 209 | res.status(500).send('User already exists'); 210 | } 211 | }); 212 | 213 | server.use('/users', (req, res, next) => { 214 | if (isAuthorized(req) || req.query.bypassAuth === 'true') { 215 | next(); 216 | } else { 217 | res.sendStatus(401); 218 | } 219 | }); 220 | 221 | server.use(router); 222 | server.listen(3000, () => { 223 | console.log('JSON Server is running'); 224 | }); 225 | 226 | function formatUser(user) { 227 | delete user.password; 228 | user.role = user.username === 'admin' 229 | ? 'admin' 230 | : 'user'; 231 | return user; 232 | } 233 | 234 | function checkIfAdmin(user, bypassToken = false) { 235 | return user.username === 'admin' || bypassToken === true 236 | ? 'admin-token' 237 | : 'user-token'; 238 | } 239 | 240 | function isAuthorized(req) { 241 | return req.headers.authorization === 'admin-token' ? true : false; 242 | } 243 | 244 | function readUsers() { 245 | const dbRaw = fs.readFileSync('./server/db.json'); 246 | const users = JSON.parse(dbRaw).users 247 | return users; 248 | } 249 | ``` 250 | 251 | * Start the server and leave it running whenever you use the apps. 252 | 253 | ``` 254 | npm run server 255 | ``` 256 | 257 | 258 | 259 | -------------------------------------------------------------------------------- /part-13-router-actions-and-effects.md: -------------------------------------------------------------------------------- 1 | # Part 13 - Router actions and effects 2 | 3 | #### 1 .Add a new presentational components for users 4 | 5 | ``` 6 | ng g c components/users-table -a=admin-portal/users 7 | ``` 8 | 9 | * Add a toolbar module 10 | 11 | ``` 12 | ng g c components/users-table-toolbar -a=admin-portal/users 13 | ``` 14 | 15 | * Add a toolbar with select field 16 | 17 | _**libs/admin-portal/users/src/components/users-table-toolbar/users-table-toolbar.component.html**_ 18 | 19 | ```html 20 | 21 | 22 | 23 | None 24 | Australia 25 | England 26 | USA 27 | 28 | 29 | 30 | ``` 31 | 32 | _**libs/admin-portal/users/src/components/users-table-toolbar/users-table-toolbar.component.ts**_ 33 | 34 | ```ts 35 | import { Component, OnInit, Output, EventEmitter } from '@angular/core'; 36 | 37 | @Component({ 38 | selector: 'users-table-toolbar', 39 | templateUrl: './users-table-toolbar.component.html', 40 | styleUrls: ['./users-table-toolbar.component.scss'] 41 | }) 42 | export class UsersTableToolbarComponent { 43 | @Output() onFilter = new EventEmitter(); 44 | selectedCountry = 'none'; 45 | 46 | filter(country:string){ 47 | this.onFilter.emit(country); 48 | } 49 | } 50 | ``` 51 | 52 | * Add a users list table component 53 | 54 | _**libs/admin-portal/users/src/components/users-table/users-table.component.ts**_ 55 | 56 | ```ts 57 | import { Component, OnInit, Input, OnChanges, SimpleChanges } from '@angular/core'; 58 | import { User } from '@demo-app/data-models'; 59 | import { MatTableDataSource } from '@angular/material'; 60 | 61 | @Component({ 62 | selector: 'app-users-table', 63 | templateUrl: './users-table.component.html', 64 | styleUrls: ['./users-table.component.scss'] 65 | }) 66 | export class UsersTableComponent implements OnChanges { 67 | @Input() users: User[]; 68 | 69 | displayedColumns = ['username', 'country']; 70 | dataSource: MatTableDataSource; 71 | 72 | ngOnChanges(changes: SimpleChanges) { 73 | this.dataSource = new MatTableDataSource(this.users); 74 | } 75 | 76 | setTableDatasource(dataSource): void { 77 | this.dataSource = new MatTableDataSource(this.users); 78 | } 79 | 80 | } 81 | ``` 82 | 83 | * Add the html 84 | 85 | _**libs/admin-portal/users/src/components/users-table/users-table.component.html**_ 86 | 87 | ```html 88 |
89 | 90 | 91 | 92 | Username 93 | {{element.username}} 94 | 95 | 96 | 97 | Country 98 | {{element.country}} 99 | 100 | 101 | 102 | 103 | 104 |
105 | ``` 106 | 107 | #### 2. Add filter actions to update state 108 | 109 | ```ts 110 | import { Action } from '@ngrx/store'; 111 | import { User } from '@demo-app/data-models'; 112 | 113 | export enum UsersActionTypes { 114 | LoadUsers = '[Users] Load', 115 | LoadUsersSuccess = '[Users] Load Sucess', 116 | LoadUsersFail = '[Users] Load Fail', 117 | SetUsersFilter = '[Users] Set Filter' 118 | } 119 | 120 | export class LoadUsersAction implements Action { 121 | readonly type = UsersActionTypes.LoadUsers; 122 | } 123 | 124 | export class LoadUsersSuccessAction implements Action { 125 | readonly type = UsersActionTypes.LoadUsersSuccess; 126 | constructor(public payload: User[]) {} 127 | } 128 | 129 | export class LoadUsersFailAction implements Action { 130 | readonly type = UsersActionTypes.LoadUsersSuccess; 131 | constructor(public payload: any) {} 132 | } 133 | 134 | export class SetUsersFiltersAction { 135 | readonly type = UsersActionTypes.SetUsersFilter; 136 | constructor(public payload: string) {} 137 | } 138 | 139 | export type UsersActions = 140 | | LoadUsersAction 141 | | LoadUsersSuccessAction 142 | | LoadUsersFailAction 143 | | SetUsersFiltersAction; 144 | ``` 145 | 146 | * Update the users state to have a selectedCountry 147 | 148 | ```ts 149 | import { EntityState } from '@ngrx/entity'; 150 | import { User } from '@demo-app/data-models'; 151 | 152 | export interface Users extends EntityState { 153 | selectedUserId: number; 154 | loading: boolean; 155 | selectedCountry: string 156 | } 157 | 158 | export interface UsersState { 159 | readonly users: Users; 160 | } 161 | ``` 162 | 163 | * Update the initial state 164 | 165 | _**libs/admin-portal/users/src/+state/users.interface.ts**_ 166 | 167 | ```ts 168 | import { Users } from './users.interfaces'; 169 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 170 | import { User } from '@demo-app/data-models'; 171 | 172 | export const adapter: EntityAdapter = createEntityAdapter({}); 173 | 174 | export const usersInitialState: Users = adapter.getInitialState({ 175 | selectedUserId: null, 176 | loading: false, 177 | selectedCountry: 'none' 178 | }); 179 | ``` 180 | 181 | * Remove load dispatch aciton from component 182 | 183 | _**libs/admin-portal/users/src/+state/users.init.ts**_ 184 | 185 | ```ts 186 | ngOnInit() { 187 | // this.store.dispatch(new usersActions.LoadUsersAction()); 188 | this.users$ = this.store.select(selectAllUsers); 189 | } 190 | ``` 191 | 192 | #### 3. Add router method to user-list component 193 | 194 | _**libs/admin-portal/users/src/containers/user-list/user-list.component.html**_ 195 | 196 | ``` 197 | 198 | 199 | ``` 200 | 201 | _**libs/admin-portal/users/src/containers/user-list/user-list.component.ts**_ 202 | 203 | ```ts 204 | updateUrlFilters(country: string): void { 205 | const navigationExtras: NavigationExtras = { 206 | replaceUrl: true, 207 | queryParams: {country} 208 | }; 209 | this.router.navigate([`/users`], navigationExtras); 210 | } 211 | ``` 212 | 213 | * Add an effect to listen for the route change 214 | 215 | ```ts 216 | @Effect() 217 | loadUsersFromRoute = this.dataPersistence.navigation(UserListComponent, { 218 | run: (a: ActivatedRouteSnapshot, state: UsersState) => { 219 | return this.usersService 220 | .getUsers(a.queryParams['country']) 221 | .pipe( 222 | map((users: User[]) => new usersActions.LoadUsersSuccessAction(users)) 223 | ); 224 | }, 225 | onError: (a: ActivatedRouteSnapshot, e: any) => { 226 | return new usersActions.LoadUsersFailAction(e); 227 | } 228 | }); 229 | ``` 230 | 231 | * Update the user service 232 | 233 | ```ts 234 | import { Injectable } from '@angular/core'; 235 | import { HttpClient } from '@angular/common/http'; 236 | 237 | @Injectable() 238 | export class UsersService { 239 | constructor(private httpClient: HttpClient) {} 240 | 241 | getUsers(country: string = null) { 242 | const url = country !== null 243 | ? `http://localhost:3000/users?country=${country}` 244 | : `http://localhost:3000/users`; 245 | 246 | return this.httpClient.get(url); 247 | } 248 | } 249 | ``` 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /part-9-nx-state-libs.md: -------------------------------------------------------------------------------- 1 | # Part 9 - nx state libs 2 | 3 | #### 1. Add a beta ngrx schematics 4 | 5 | ``` 6 | npm i github:ngrx/schematics-builds --save-dev 7 | ``` 8 | 9 | Note: The new schematics from the offical ngrx team are in testing but can be good for now while we find out if the nx team swap to action creators [https://github.com/ngrx/platform/issues/674](https://github.com/ngrx/platform/issues/674) 10 | 11 | #### 2. Add auth ngrx state 12 | 13 | ``` 14 | ng generate ngrx auth --module=libs/auth/src/auth.module.ts 15 | ``` 16 | 17 | * Delete the actions file from the nx code generated files at the path _**libs/auth/src/+state/auth.actions.ts**_ 18 | 19 | ``` 20 | ng generate action Auth --collection @ngrx/schematics 21 | ``` 22 | 23 | * Swap out original for new nx style effect 24 | 25 | _**libs/auth/src/+state/auth.actions.ts**_ 26 | 27 | ```ts 28 | import { Action } from '@ngrx/store'; 29 | import { User, Authenticate } from '@demo-app/data-models'; 30 | 31 | export enum AuthStateActionTypes { 32 | Login = '[AuthState] Login', 33 | LoginSuccess = '[AuthState] Login Success', 34 | LoginFail = '[AuthState] Login Fail' 35 | } 36 | 37 | export class LoginAction implements Action { 38 | readonly type = AuthStateActionTypes.Login; 39 | constructor(public payload: Authenticate) {} 40 | } 41 | 42 | export class LoginSuccessAction implements Action { 43 | readonly type = AuthStateActionTypes.LoginSuccess; 44 | constructor(public payload: User) {} 45 | } 46 | 47 | export class LoginFailAction implements Action { 48 | readonly type = AuthStateActionTypes.LoginFail; 49 | constructor(public payload) {} 50 | } 51 | 52 | export type AuthStateActions = 53 | LoginAction 54 | | LoginFailAction 55 | | LoginSuccessAction; 56 | ``` 57 | 58 | ### 3. Add an effect 59 | 60 | _**libs/auth/src/+state/auth.effects.ts**_ 61 | 62 | ```ts 63 | import { Injectable } from '@angular/core'; 64 | import { Effect, Actions } from '@ngrx/effects'; 65 | import { of } from 'rxjs/observable/of'; 66 | import 'rxjs/add/operator/switchMap'; 67 | import { AuthState } from './auth.interfaces'; 68 | import * as authActions from './auth.actions'; 69 | import { map, catchError, tap, mergeMap } from 'rxjs/operators'; 70 | import { AuthService } from './../services/auth.service'; 71 | 72 | @Injectable() 73 | export class AuthEffects { 74 | @Effect() 75 | login = this.actions$ 76 | .ofType(authActions.AuthStateActionTypes.Login) 77 | .pipe( 78 | mergeMap((action: authActions.LoginAction) => 79 | this.authService 80 | .login(action.payload) 81 | .pipe( 82 | map(user => new authActions.LoginSuccessAction(user)), 83 | catchError(error => of(new authActions.LoginFailAction(error))) 84 | ) 85 | ) 86 | ); 87 | 88 | constructor(private actions$: Actions, private authService: AuthService) {} 89 | } 90 | ``` 91 | 92 | * Swap out original for new nx style effect 93 | 94 | _**libs/auth/src/+state/auth.effects.ts**_ 95 | 96 | ```ts 97 | import { Injectable } from '@angular/core'; 98 | import { Effect, Actions } from '@ngrx/effects'; 99 | import { of } from 'rxjs/observable/of'; 100 | import 'rxjs/add/operator/switchMap'; 101 | import { AuthState } from './auth.interfaces'; 102 | import * as authActions from './auth.actions'; 103 | import { map, catchError, tap, mergeMap } from 'rxjs/operators'; 104 | import { AuthService } from './../services/auth.service'; 105 | import { DataPersistence } from '@nrwl/nx'; 106 | 107 | @Injectable() 108 | export class AuthEffects { 109 | @Effect() 110 | login$ = this.dataPersistence.fetch(authActions.AuthStateActionTypes.Login, { 111 | run: (action: authActions.LoginAction, state: AuthState) => { 112 | return this.authService 113 | .login(action.payload) 114 | .pipe(map(user => new authActions.LoginSuccessAction(user))); 115 | }, 116 | 117 | onError: (action: authActions.LoginAction, error) => { 118 | return of(new authActions.LoginFailAction(error)); 119 | } 120 | }); 121 | 122 | constructor( 123 | private actions: Actions, 124 | private dataPersistence: DataPersistence, 125 | private authService: AuthService 126 | ) {} 127 | } 128 | ``` 129 | 130 | #### 3. Add default state and interface 131 | 132 | * Update state interface 133 | 134 | _**libs/auth/src/+state/auth.interfaces.ts**_ 135 | 136 | ```ts 137 | import { User } from "@demo-app/data-models"; 138 | 139 | export interface Auth { 140 | user: User, 141 | loading: boolean 142 | } 143 | 144 | export interface AuthState { 145 | readonly auth: Auth; 146 | } 147 | ``` 148 | 149 | * Update state init 150 | 151 | _**libs/auth/src/+state/auth.init.ts**_ 152 | 153 | ```ts 154 | import { Auth } from './auth.interfaces'; 155 | 156 | export const authInitialState: Auth = { 157 | user: null, 158 | loading: false 159 | }; 160 | ``` 161 | 162 | #### 4. Add reducer code 163 | 164 | _**libs/auth/src/+state/auth.reducer.ts**_ 165 | 166 | ```ts 167 | import { Auth } from './auth.interfaces'; 168 | import * as authActions from './auth.actions'; 169 | 170 | export function authReducer( 171 | state: Auth, 172 | action: authActions.AuthStateActions 173 | ): Auth { 174 | switch (action.type) { 175 | case authActions.AuthStateActionTypes.Login: { 176 | return { 177 | ...state, 178 | loading: true 179 | }; 180 | } 181 | case authActions.AuthStateActionTypes.LoginSuccess: { 182 | return { 183 | ...state, 184 | loading: false, 185 | user: action.payload 186 | }; 187 | } 188 | default: { 189 | return state; 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | #### 5. Update LoginComponent to dispatch action 196 | 197 | _**libs/auth/src/containers/login/login.component.ts**_ 198 | 199 | ```ts 200 | import { Component, OnInit } from '@angular/core'; 201 | import { FormGroup, FormControl, Validators } from '@angular/forms';import { User, Authenticate } from '@demo-app/data-models'; 202 | import { Store } from '@ngrx/store'; 203 | import { AuthState } from './../../+state/auth.interfaces'; 204 | import * as authActions from './../../+state/auth.actions'; 205 | 206 | @Component({ 207 | selector: 'app-login', 208 | templateUrl: './login.component.html', 209 | styleUrls: ['./login.component.scss'] 210 | }) 211 | export class LoginComponent implements OnInit { 212 | constructor(private store: Store) {} 213 | 214 | ngOnInit() {} 215 | 216 | login(authenticate: Authenticate): void { 217 | this.store.dispatch(new authActions.LoginAction(authenticate)); 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | #### 6. Add route change action on success 224 | 225 | * Add a new action to navigate 226 | 227 | _**libs/auth/src/+state/auth.actions.ts**_ 228 | 229 | ```ts 230 | import { Action } from '@ngrx/store'; 231 | import { User, Authenticate } from '@demo-app/data-models'; 232 | 233 | export enum AuthStateActionTypes { 234 | Login = '[AuthState] Login', 235 | LoginSuccess = '[AuthState] Login Success', 236 | LoginFail = '[AuthState] Login Fail', 237 | NavigateToProfile = '[AuthState] Navigate To Profile' 238 | } 239 | 240 | export class LoginAction implements Action { 241 | readonly type = AuthStateActionTypes.Login; 242 | constructor(public payload: Authenticate) {} 243 | } 244 | 245 | export class LoginSuccessAction implements Action { 246 | readonly type = AuthStateActionTypes.LoginSuccess; 247 | constructor(public payload: User) {} 248 | } 249 | 250 | export class LoginFailAction implements Action { 251 | readonly type = AuthStateActionTypes.LoginFail; 252 | constructor(public payload) {} 253 | } 254 | 255 | export class NavigateToProfileAction implements Action { 256 | readonly type = AuthStateActionTypes.NavigateToProfile; 257 | constructor(public payload:number) {} 258 | } 259 | 260 | export type AuthStateActions = 261 | LoginAction 262 | | LoginFailAction 263 | | LoginSuccessAction 264 | | NavigateToProfileAction; 265 | ``` 266 | 267 | * Add new Effect action to navigate 268 | 269 | _**libs/auth/src/+state/auth.effects.ts**_ 270 | 271 | ```ts 272 | import { Injectable } from '@angular/core'; 273 | import { Effect, Actions } from '@ngrx/effects'; 274 | import { of } from 'rxjs/observable/of'; 275 | import 'rxjs/add/operator/switchMap'; 276 | import { AuthState } from './auth.interfaces'; 277 | import * as authActions from './auth.actions'; 278 | import { map, catchError, tap, mergeMap } from 'rxjs/operators'; 279 | import { AuthService } from './../services/auth.service'; 280 | import { DataPersistence } from '@nrwl/nx'; 281 | import { Router } from '@angular/router'; 282 | import { User } from '@demo-app/data-models'; 283 | 284 | @Injectable() 285 | export class AuthEffects { 286 | @Effect() 287 | login$ = this.dataPersistence.fetch(authActions.AuthStateActionTypes.Login, { 288 | run: (action: authActions.LoginAction, state: AuthState) => { 289 | return this.authService 290 | .login(action.payload) 291 | .pipe( 292 | mergeMap((user: User) => [ 293 | new authActions.LoginSuccessAction(user), 294 | new authActions.NavigateToProfileAction(user.id) 295 | ]) 296 | ); 297 | }, 298 | 299 | onError: (action: authActions.LoginAction, error) => { 300 | return of(new authActions.LoginFailAction(error)); 301 | } 302 | }); 303 | 304 | @Effect({ dispatch: false }) 305 | navigateToProfile = this.actions 306 | .ofType(authActions.AuthStateActionTypes.NavigateToProfile) 307 | .pipe( 308 | map((action: authActions.NavigateToProfileAction) => 309 | this.router.navigate([`/user-profile/${action.payload}`]) 310 | ) 311 | ); 312 | 313 | constructor( 314 | private actions: Actions, 315 | private dataPersistence: DataPersistence, 316 | private authService: AuthService, 317 | private router: Router 318 | ) {} 319 | } 320 | ``` 321 | 322 | #### 7. Export state references in index.ts 323 | 324 | _**libs/auth/index.ts**_ 325 | 326 | ```ts 327 | export { AuthModule, authRoutes } from './src/auth.module'; 328 | export { AuthState } from './src/+state/auth.interfaces'; 329 | // export * from './src/+state'; 330 | ``` 331 | 332 | 333 | 334 | -------------------------------------------------------------------------------- /part-7-routing-and-interceptors.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | --- 5 | 6 | # Part 7 - Routing and interceptors 7 | 8 | #### 1.Add a lib for a users profile page 9 | 10 | * Add a lazy loaded lib with routing. Note this will add linting rules to .angular-cli.json to stop adding this module to other modules. 11 | 12 | ``` 13 | ng g lib user-profile --routing --lazy --parent-module=apps/customer-portal/src/app/app.module.ts 14 | ``` 15 | 16 | * Add a user-profile container component. 17 | 18 | ``` 19 | ng g c containers/user-profile -a=user-profile 20 | ``` 21 | 22 | #### 2. Add a method in the subscription to navigate to the login page on login 23 | 24 | * Run the following command to navigate 25 | 26 | _**libs/auth/src/containers/login/login.component.ts**_ 27 | 28 | ```ts 29 | import { Component, OnInit } from '@angular/core'; 30 | import { AuthService } from './../../services/auth.service'; 31 | import { Authenticate, User } from '@demo-app/data-models'; 32 | import { Router } from '@angular/router'; 33 | 34 | @Component({ 35 | selector: 'app-login', 36 | templateUrl: './login.component.html', 37 | styleUrls: ['./login.component.scss'] 38 | }) 39 | export class LoginComponent implements OnInit { 40 | constructor(private router: Router, private authService: AuthService) {} 41 | 42 | ngOnInit() {} 43 | 44 | login(authenticate: Authenticate) { 45 | this.authService 46 | .login(authenticate) 47 | .subscribe((user: User) => 48 | this.router.navigate([`/user-profile/${user.id}`]) 49 | ); 50 | } 51 | } 52 | ``` 53 | 54 | * Add default routes to app module 55 | 56 | _**apps/customer-portal/src/app/app.module.ts**_ 57 | 58 | ```ts 59 | RouterModule.forRoot( 60 | [ 61 | { path: '', pathMatch: 'full', redirectTo: 'user-profile' }, 62 | { path: 'auth', children: authRoutes }, 63 | { 64 | path: 'user-profile', 65 | loadChildren: '@demo-app/user-profile#UserProfileModule' 66 | } 67 | ], 68 | { 69 | initialNavigation: 'enabled' 70 | } 71 | ), 72 | ``` 73 | 74 | _**libs/user-profile/src/user-profile.module.ts**_ 75 | 76 | ```ts 77 | import { NgModule } from '@angular/core'; 78 | import { CommonModule } from '@angular/common'; 79 | import { UserProfileComponent } from './containers/user-profile/user-profile.component'; 80 | import { RouterModule } from '@angular/router'; 81 | 82 | @NgModule({ 83 | imports: [ 84 | CommonModule, 85 | RouterModule.forChild([{ path: ':id', component: UserProfileComponent }]) 86 | ], 87 | declarations: [UserProfileComponent] 88 | }) 89 | export class UserProfileModule {} 90 | ``` 91 | 92 | #### 3. Add a route guard to protect profile page 93 | 94 | * Generate a guard wit the CLI 95 | 96 | ``` 97 | ng g guard guards/auth/auth -a=auth 98 | ``` 99 | 100 | * Add a static forRoot method and register services and the guard 101 | 102 | _**libs/auth/src/auth.module.ts**_ 103 | 104 | ```ts 105 | import { NgModule, ModuleWithProviders } from '@angular/core'; 106 | import { CommonModule } from '@angular/common'; 107 | import { RouterModule, Route } from '@angular/router'; 108 | import { LoginComponent } from './containers/login/login.component'; 109 | import { HttpClientModule } from '@angular/common/http'; 110 | import { AuthService } from './services/auth.service'; 111 | import { MaterialModule } from '@demo-app/material'; 112 | import { ReactiveFormsModule } from '@angular/forms'; 113 | import { AuthGuard } from './guards/auth/auth.guard'; 114 | import { LoginFormComponent } from './components/login-form/login-form.component'; 115 | 116 | export const authRoutes: Route[] = [ 117 | { path: 'login', component: LoginComponent } 118 | ]; 119 | const COMPONENTS = [LoginComponent, LoginFormComponent]; 120 | 121 | @NgModule({ 122 | imports: [ 123 | CommonModule, 124 | RouterModule, 125 | HttpClientModule, 126 | MaterialModule, 127 | ReactiveFormsModule 128 | ], 129 | declarations: [COMPONENTS], 130 | exports: [COMPONENTS], 131 | providers: [AuthService, AuthGuard] 132 | }) 133 | export class AuthModule { 134 | static forRoot(): ModuleWithProviders { 135 | return { 136 | ngModule: AuthModule, 137 | providers: [AuthService, AuthGuard] 138 | }; 139 | } 140 | } 141 | ``` 142 | 143 | * update the authService to set a local flag before we add ngrx later 144 | 145 | _**libs/auth/src/services/auth.service.ts**_ 146 | 147 | ```ts 148 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 149 | import { Injectable } from '@angular/core'; 150 | import { Authenticate } from '@demo-app/data-models'; 151 | import { tap } from 'rxjs/operators'; 152 | 153 | @Injectable() 154 | export class AuthService { 155 | isAuthenticated: boolean; 156 | constructor(private httpClient: HttpClient) {} 157 | 158 | login(authenticate: Authenticate) { 159 | return this.httpClient 160 | .post('http://localhost:3000/login', authenticate) 161 | .pipe(tap(user => (this.isAuthenticated = true))); 162 | } 163 | } 164 | ``` 165 | 166 | * Add auth guard logic 167 | 168 | _**libs/auth/src/guards/auth/auth.guard.ts**_ 169 | 170 | ```ts 171 | import { Injectable } from '@angular/core'; 172 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 173 | import { Observable } from 'rxjs/Observable'; 174 | import { AuthService } from './../../services/auth.service'; 175 | 176 | @Injectable() 177 | export class AuthGuard implements CanActivate { 178 | 179 | constructor( 180 | private router: Router, 181 | private authService: AuthService) {} 182 | 183 | canActivate( 184 | next: ActivatedRouteSnapshot, 185 | state: RouterStateSnapshot): Observable | Promise | boolean { 186 | if(this.authService.isAuthenticated) { 187 | return true; 188 | } else { 189 | this.router.navigate([`/auth/login`]); 190 | return false; 191 | } 192 | } 193 | } 194 | ``` 195 | 196 | * add auth guard to main routes 197 | 198 | _**apps/customer-portal/src/app/app.module.ts**_ 199 | 200 | ```ts 201 | import { NgModule } from '@angular/core'; 202 | import { AppComponent } from './app.component'; 203 | import { BrowserModule } from '@angular/platform-browser'; 204 | import { NxModule } from '@nrwl/nx'; 205 | import { RouterModule } from '@angular/router'; 206 | import { StoreModule } from '@ngrx/store'; 207 | import { EffectsModule } from '@ngrx/effects'; 208 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 209 | import { environment } from '../environments/environment'; 210 | import { StoreRouterConnectingModule } from '@ngrx/router-store'; 211 | import { authRoutes, AuthModule } from '@demo-app/auth'; 212 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 213 | import { AuthGuard } from '@demo-app/auth'; 214 | 215 | @NgModule({ 216 | imports: [ 217 | BrowserModule, 218 | NxModule.forRoot(), 219 | RouterModule.forRoot( 220 | [ 221 | { path: '', pathMatch: 'full', redirectTo: 'user-profile' }, 222 | { path: 'auth', children: authRoutes }, 223 | { 224 | path: 'user-profile', 225 | loadChildren: '@demo-app/user-profile#UserProfileModule', 226 | canActivate: [AuthGuard] 227 | } 228 | ], 229 | { 230 | initialNavigation: 'enabled' 231 | } 232 | ), 233 | StoreModule.forRoot({}), 234 | EffectsModule.forRoot([]), 235 | !environment.production ? StoreDevtoolsModule.instrument() : [], 236 | StoreRouterConnectingModule, 237 | AuthModule.forRoot(), 238 | BrowserAnimationsModule 239 | ], 240 | declarations: [AppComponent], 241 | bootstrap: [AppComponent] 242 | }) 243 | export class AppModule {} 244 | ``` 245 | 246 | #### 4. Add angular interceptor 247 | 248 | * Update auth service to set a token in local storage 249 | 250 | _**libs/auth/src/services/auth.service.ts**_ 251 | 252 | ```ts 253 | import { Injectable } from '@angular/core'; 254 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 255 | import { tap } from 'rxjs/operators'; 256 | import { User, Authenticate } from '@demo-app/data-models'; 257 | 258 | @Injectable() 259 | export class AuthService { 260 | user: User; 261 | isAuthenticated: boolean; 262 | 263 | constructor(private httpClient: HttpClient) {} 264 | 265 | login(authenticate: Authenticate) { 266 | return this.httpClient 267 | .post('http://localhost:3000/login', authenticate) 268 | .pipe( 269 | tap((user: User) => { 270 | this.isAuthenticated = true; 271 | this.user = user; 272 | this.setAuthToken(user.token); 273 | }) 274 | ); 275 | } 276 | 277 | setAuthToken(token: string) { 278 | localStorage.setItem('token', token); 279 | } 280 | 281 | getAuthToken(): string { 282 | return localStorage.getItem('token'); 283 | } 284 | 285 | clearAuthToken() { 286 | localStorage.removeItem('token'); 287 | } 288 | } 289 | ``` 290 | 291 | * Add an angular interceptor in a new folder in the auth lib 292 | 293 | _**libs/auth/src/interceptors/auth.interceptor.ts**_ 294 | 295 | ```ts 296 | import { Injectable, Injector } from '@angular/core'; 297 | import { 298 | HttpEvent, 299 | HttpInterceptor, 300 | HttpHandler, 301 | HttpRequest 302 | } from '@angular/common/http'; 303 | import { Observable } from 'rxjs/Observable'; 304 | import { AuthService } from '../services/auth.service'; 305 | import { switchMap, tap } from 'rxjs/operators'; 306 | import { of } from 'rxjs/Observable/of'; 307 | 308 | @Injectable() 309 | export class AuthInterceptor implements HttpInterceptor { 310 | authService: AuthService; 311 | constructor(private injector: Injector) {} 312 | 313 | intercept( 314 | req: HttpRequest, 315 | next: HttpHandler 316 | ): Observable> { 317 | this.authService = this.injector.get(AuthService); 318 | const token = this.authService.getAuthToken(); 319 | 320 | if (token) { 321 | const authReq = req.clone({ 322 | headers: req.headers.set('Authorization', token) 323 | }); 324 | return next.handle(authReq); 325 | } else { 326 | return next.handle(req); 327 | } 328 | } 329 | } 330 | ``` 331 | 332 | * Export auth interceptors from the auth module 333 | 334 | _**libs/auth/src/auth.module.ts**_ 335 | 336 | ```ts 337 | import { NgModule, ModuleWithProviders } from '@angular/core'; 338 | import { CommonModule } from '@angular/common'; 339 | import { RouterModule, Route } from '@angular/router'; 340 | import { LoginComponent } from './container/login/login.component'; 341 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 342 | import { AuthService } from './services/auth.service'; 343 | import { MaterialModule } from '@demo-app/material'; 344 | import { ReactiveFormsModule } from '@angular/forms'; 345 | import { AuthGuard } from './/guards/auth.guard'; 346 | import { AuthInterceptor } from './interceptors/auth.interceptor'; 347 | 348 | export const authRoutes: Route[] = [ 349 | { path: 'login', component: LoginComponent } 350 | ]; 351 | const COMPONENTS = [LoginComponent]; 352 | 353 | @NgModule({ 354 | imports: [ 355 | CommonModule, 356 | RouterModule, 357 | HttpClientModule, 358 | MaterialModule, 359 | ReactiveFormsModule 360 | ], 361 | declarations: [COMPONENTS], 362 | exports: [COMPONENTS], 363 | providers: [ 364 | AuthService, 365 | AuthGuard 366 | ] 367 | }) 368 | export class AuthModule { 369 | static forRoot(): ModuleWithProviders { 370 | return { 371 | ngModule: AuthModule, 372 | providers: [ 373 | AuthService, 374 | AuthGuard, 375 | { 376 | provide: HTTP_INTERCEPTORS, 377 | useClass: AuthInterceptor, 378 | multi: true 379 | } 380 | ] 381 | }; 382 | } 383 | } 384 | ``` 385 | 386 | 387 | 388 | -------------------------------------------------------------------------------- /part-12-entity-state-adapter.md: -------------------------------------------------------------------------------- 1 | # Part 12 - Entity state adapter 2 | 3 | #### 1. Add a new route guard 4 | 5 | ``` 6 | ng g guard guards/auth-admin/auth-admin -a=auth 7 | ``` 8 | 9 | * check in the route guard if the person is an admin 10 | 11 | _**libs/auth/src/guards/auth-admin/auth-admin.guard.ts**_ 12 | 13 | ```ts 14 | import { Injectable } from '@angular/core'; 15 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; 16 | import { Observable } from 'rxjs/Observable'; 17 | import { AuthService } from './../../services/auth.service'; 18 | import { AuthState, getUser } from '@demo-app/auth'; 19 | import { Store } from '@ngrx/store'; 20 | import { map } from 'rxjs/operators'; 21 | import { User } from '@demo-app/data-models'; 22 | 23 | @Injectable() 24 | export class AuthAdminGuard implements CanActivate { 25 | 26 | constructor( 27 | private router: Router, 28 | private store: Store) {} 29 | 30 | canActivate( 31 | next: ActivatedRouteSnapshot, 32 | state: RouterStateSnapshot): Observable { 33 | return this.store.select(getUser).pipe( 34 | map((user: User) => { 35 | if(user && user.role === 'admin') { 36 | return true; 37 | } else { 38 | this.router.navigate([`/auth/login`]); 39 | return false; 40 | } 41 | }) 42 | ) 43 | } 44 | } 45 | ``` 46 | 47 | #### 2. Make a users lib in an admin directory 48 | 49 | ``` 50 | ng g lib users --directory=admin-portal --routing --lazy --parent-module=apps/admin-portal/src/app/app.module.ts 51 | ``` 52 | 53 | * Add a UserList container component 54 | 55 | ``` 56 | ng g c containers/user-list -a=admin-portal/users 57 | ``` 58 | 59 | * Add ngrx feature state to the new lib 60 | 61 | ``` 62 | ng g ngrx users --module=libs/admin-portal/users/src/users.module.ts 63 | ``` 64 | 65 | * Add UserList component to the routes for this lib 66 | 67 | _**libs/admin-portal/users/src/containers/user-list**_ 68 | 69 | ```ts 70 | @NgModule({ 71 | imports: [ 72 | CommonModule, 73 | RouterModule.forChild([ 74 | { path: '', pathMatch: 'full', component: UserListComponent } 75 | ]), 76 | StoreModule.forFeature('users', usersReducer, { 77 | initialState: usersInitialState 78 | }), 79 | EffectsModule.forFeature([UsersEffects]) 80 | ], 81 | declarations: [UserListComponent], 82 | providers: [UsersEffects] 83 | }) 84 | export class UsersModule {} 85 | ``` 86 | 87 | * Add the route also to the admin-portal app 88 | 89 | _**apps/admin-portal/src/app/app.module.ts**_ 90 | 91 | ```ts 92 | RouterModule.forRoot([ 93 | { path: '', pathMatch: 'full', redirectTo: 'user-profile' }, 94 | { path: 'auth', children: authRoutes }, 95 | { 96 | path: 'user-profile', 97 | loadChildren: '@demo-app/user-profile#UserProfileModule', 98 | canActivate: [AuthGuard] 99 | }, 100 | { 101 | path: 'users', 102 | loadChildren: '@demo-app/admin-portal/users#UsersModule', 103 | canActivate: [AuthAdminGuard] 104 | } 105 | ]), 106 | ``` 107 | 108 | #### 3. Update auth module to use new guard 109 | 110 | ```ts 111 | import { NgModule, ModuleWithProviders } from '@angular/core'; 112 | import { CommonModule } from '@angular/common'; 113 | import { RouterModule, Route } from '@angular/router'; 114 | import { LoginComponent } from './container/login/login.component'; 115 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 116 | import { AuthService } from './services/auth.service'; 117 | import { MaterialModule } from '@demo-app/material'; 118 | import { ReactiveFormsModule } from '@angular/forms'; 119 | import { AuthGuard } from './guards/auth.guard'; 120 | import { AuthInterceptor } from './interceptors/auth.interceptor'; 121 | import { StoreModule } from '@ngrx/store'; 122 | import { EffectsModule } from '@ngrx/effects'; 123 | import { authReducer } from './+state/auth.reducer'; 124 | import { authInitialState } from './+state/auth.init'; 125 | import { AuthEffects } from './+state/auth.effects'; 126 | import { AuthAdminGuard } from './guards/auth-admin/auth-admin.guard'; 127 | 128 | export const authRoutes: Route[] = [ 129 | { path: 'login', component: LoginComponent } 130 | ]; 131 | const COMPONENTS = [LoginComponent]; 132 | 133 | @NgModule({ 134 | imports: [ 135 | CommonModule, 136 | RouterModule, 137 | HttpClientModule, 138 | MaterialModule, 139 | ReactiveFormsModule, 140 | StoreModule.forFeature('auth', authReducer, {initialState: authInitialState}), 141 | EffectsModule.forFeature([AuthEffects]) 142 | ], 143 | declarations: [COMPONENTS], 144 | exports: [COMPONENTS], 145 | providers: [ 146 | AuthService, 147 | AuthGuard, 148 | AuthAdminGuard, 149 | AuthEffects 150 | ] 151 | }) 152 | export class AuthModule { 153 | static forRoot(): ModuleWithProviders { 154 | return { 155 | ngModule: AuthModule, 156 | providers: [ 157 | AuthService, 158 | AuthGuard, 159 | AuthAdminGuard, 160 | { 161 | provide: HTTP_INTERCEPTORS, 162 | useClass: AuthInterceptor, 163 | multi: true 164 | } 165 | ] 166 | }; 167 | } 168 | } 169 | ``` 170 | 171 | * Update layout lib to show users menu button in an admin 172 | 173 | _**libs/admin-portal/layout/src/containers/layout/layout.component.html**_ 174 | 175 | ```html 176 | 177 | Admin Portal 178 |
179 | {{(user$ | async)?.username}} 180 | 181 |
182 |
183 | 184 | ``` 185 | 186 | #### 4. Make a new service for users 187 | 188 | ``` 189 | ng g service services/users -a=admin-portal/users 190 | ``` 191 | 192 | * add a new method to the service to get users 193 | 194 | ```ts 195 | import { Injectable } from '@angular/core'; 196 | import { HttpClient } from '@angular/common/http'; 197 | 198 | @Injectable() 199 | export class UsersService { 200 | 201 | constructor(private httpClient: HttpClient) { } 202 | 203 | getUsers() { 204 | return this.httpClient.get(`http://localhost:3000/users`); 205 | } 206 | 207 | } 208 | ``` 209 | 210 | #### 5. Configure ngrx state for users 211 | 212 | * Delete the actions file for users and use the ngrx teams generator to make it. 213 | * Change directory in the file path of your terminal before you run this command to be where you want the file 214 | 215 | ``` 216 | cd libs/admin-portal/users/src/+state/ 217 | ``` 218 | 219 | ```ts 220 | ng g action users --collection @ngrx/schematics 221 | ``` 222 | 223 | * Update the actions for getting users 224 | 225 | _**libs/admin-portal/users/src/+state/users.actions.ts**_ 226 | 227 | ```ts 228 | import { Action } from '@ngrx/store'; 229 | import { User } from '@demo-app/data-models'; 230 | 231 | export enum UsersActionTypes { 232 | LoadUsers = '[Users] Load', 233 | LoadUsersSuccess = '[Users] Load Sucess', 234 | LoadUsersFail = '[Users] Load Fail' 235 | } 236 | 237 | export class LoadUsersAction implements Action { 238 | readonly type = UsersActionTypes.LoadUsers; 239 | } 240 | 241 | export class LoadUsersSuccessAction implements Action { 242 | readonly type = UsersActionTypes.LoadUsersSuccess; 243 | constructor(public payload: User[]) {} 244 | } 245 | 246 | export class LoadUsersFailAction implements Action { 247 | readonly type = UsersActionTypes.LoadUsersFail; 248 | constructor(public payload: any) {} 249 | } 250 | 251 | export type UsersActions = 252 | | LoadUsersAction 253 | | LoadUsersSuccessAction 254 | | LoadUsersFailAction; 255 | ``` 256 | 257 | * Update the default effect logic 258 | 259 | _**libs/admin-portal/users/src/+state/users.effects.ts**_ 260 | 261 | ```ts 262 | import { Injectable } from '@angular/core'; 263 | import { Effect, Actions } from '@ngrx/effects'; 264 | import { DataPersistence } from '@nrwl/nx'; 265 | import { of } from 'rxjs/observable/of'; 266 | import 'rxjs/add/operator/switchMap'; 267 | import { UsersState } from './users.interfaces'; 268 | import * as usersActions from './users.actions'; 269 | import { UsersService } from './../services/users.service'; 270 | import { map } from 'rxjs/operators'; 271 | import { User } from '@demo-app/data-models'; 272 | 273 | @Injectable() 274 | export class UsersEffects { 275 | @Effect() 276 | loadData = this.dataPersistence.fetch( 277 | usersActions.UsersActionTypes.LoadUsers, 278 | { 279 | run: (action: usersActions.LoadUsersAction, state: UsersState) => { 280 | return this.usersService 281 | .getUsers() 282 | .pipe( 283 | map( 284 | (users: User[]) => new usersActions.LoadUsersSuccessAction(users) 285 | ) 286 | ); 287 | }, 288 | 289 | onError: (action: usersActions.LoadUsersAction, error) => 290 | new usersActions.LoadUsersFailAction(error) 291 | } 292 | ); 293 | 294 | constructor( 295 | private actions: Actions, 296 | private dataPersistence: DataPersistence, 297 | private usersService: UsersService 298 | ) {} 299 | } 300 | ``` 301 | 302 | * Install ngrx's entity library 303 | 304 | ``` 305 | npm install @ngrx/entity 306 | ``` 307 | 308 | * Add interface information 309 | 310 | _**libs/admin-portal/users/+state/interfaces.init.ts**_ 311 | 312 | ```ts 313 | import { EntityState } from '@ngrx/entity'; 314 | import { User } from '@demo-app/data-models'; 315 | 316 | export interface Users extends EntityState { 317 | selectedUserId: number; 318 | loading: boolean; 319 | } 320 | 321 | export interface UsersState { 322 | readonly users: Users; 323 | } 324 | ``` 325 | 326 | * Add default state 327 | 328 | _**libs/admin-portal/users/+state/users.init.ts**_ 329 | 330 | ```ts 331 | import { Users } from './users.interfaces'; 332 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 333 | import { User } from '@demo-app/data-models'; 334 | 335 | export const adapter: EntityAdapter = createEntityAdapter(); 336 | 337 | export const usersInitialState: Users = adapter.getInitialState({ 338 | selectedUserId: null, 339 | loading: false 340 | }); 341 | ``` 342 | 343 | * Update the default reducer logic 344 | 345 | _**libs/admin-portal/users/+state/users.reducer.ts**_ 346 | 347 | ```ts 348 | import { Users } from './users.interfaces'; 349 | import * as usersActions from './users.actions'; 350 | import { adapter } from './users.init'; 351 | 352 | export function usersReducer( 353 | state: Users, 354 | action: usersActions.UsersActions 355 | ): Users { 356 | switch (action.type) { 357 | case usersActions.UsersActionTypes.LoadUsers: { 358 | return { ...state, loading: true }; 359 | } 360 | 361 | case usersActions.UsersActionTypes.LoadUsersSuccess: { 362 | return adapter.addAll(action.payload, { ...state, loading: false }); 363 | } 364 | 365 | default: { 366 | return state; 367 | } 368 | } 369 | } 370 | 371 | export const getSelectedUserId = (state: Users) => state.selectedUserId; 372 | 373 | export const { 374 | // select the array of user ids 375 | selectIds: selectUserIds, 376 | 377 | // select the dictionary of user entities 378 | selectEntities: selectUserEntities, 379 | 380 | // select the array of users 381 | selectAll: selectAllUsers, 382 | 383 | // select the total user count 384 | selectTotal: selectUserTotal 385 | } = adapter.getSelectors(); 386 | ``` 387 | 388 | * Add default selectors to use entity adapter 389 | 390 | _**libs/admin-portal/users/src/+state/index.ts**_ 391 | 392 | ```ts 393 | import { createSelector, createFeatureSelector, ActionReducerMap } from '@ngrx/store'; 394 | import * as fromUsers from './users.reducer'; 395 | import { Users } from './users.interfaces'; 396 | 397 | export const selectUserState = createFeatureSelector('users'); 398 | 399 | export const selectUserIds = createSelector(selectUserState, fromUsers.selectUserIds); 400 | export const selectUserEntities = createSelector(selectUserState, fromUsers.selectUserEntities); 401 | export const selectAllUsers = createSelector(selectUserState, fromUsers.selectAllUsers); 402 | export const selectUserTotal = createSelector(selectUserState, fromUsers.selectUserTotal); 403 | export const selectCurrentUserId = createSelector(selectUserState, fromUsers.getSelectedUserId); 404 | 405 | export const selectCurrentUser = createSelector( 406 | selectUserEntities, 407 | selectCurrentUserId, 408 | (userEntities, userId) => userEntities[userId] 409 | ); 410 | ``` 411 | 412 | * Check the whole things works and dump the users onto the page 413 | 414 | _**libs/admin-portal/users/containers/user-list.component.html**_ 415 | 416 | ```ts 417 | {{users$ | async | json}} 418 | ``` 419 | 420 | 421 | 422 | --------------------------------------------------------------------------------