├── .gitignore ├── AUTH_CODE_LAB.md ├── NgRxWorkshop.pdf ├── README.md ├── angular.json ├── browserslist ├── db.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── auth │ │ ├── actions │ │ │ ├── auth-api.actions.ts │ │ │ ├── auth-user.actions.ts │ │ │ └── index.ts │ │ ├── auth.effects.ts │ │ ├── auth.module.ts │ │ ├── components │ │ │ ├── login-form │ │ │ │ ├── index.ts │ │ │ │ ├── login-form.component.css │ │ │ │ ├── login-form.component.html │ │ │ │ ├── login-form.component.ts │ │ │ │ └── login-form.module.ts │ │ │ ├── login-page │ │ │ │ ├── index.ts │ │ │ │ ├── login-page.component.css │ │ │ │ ├── login-page.component.html │ │ │ │ ├── login-page.component.ts │ │ │ │ └── login-page.module.ts │ │ │ └── user │ │ │ │ ├── index.ts │ │ │ │ ├── user.component.css │ │ │ │ ├── user.component.html │ │ │ │ ├── user.component.ts │ │ │ │ └── user.module.ts │ │ └── index.ts │ ├── books │ │ ├── actions │ │ │ ├── books-api.actions.ts │ │ │ ├── books-page.actions.ts │ │ │ └── index.ts │ │ ├── books-api.effects.ts │ │ ├── books.module.ts │ │ ├── components │ │ │ ├── book-detail │ │ │ │ ├── book-detail.component.css │ │ │ │ ├── book-detail.component.html │ │ │ │ └── book-detail.component.ts │ │ │ ├── books-list │ │ │ │ ├── books-list.component.css │ │ │ │ ├── books-list.component.html │ │ │ │ └── books-list.component.ts │ │ │ ├── books-page │ │ │ │ ├── books-page.component.css │ │ │ │ ├── books-page.component.html │ │ │ │ └── books-page.component.ts │ │ │ └── books-total │ │ │ │ ├── books-total.component.css │ │ │ │ ├── books-total.component.html │ │ │ │ └── books-total.component.ts │ │ └── index.ts │ ├── material.module.ts │ └── shared │ │ ├── models │ │ ├── book.model.ts │ │ ├── index.ts │ │ └── user.model.ts │ │ ├── services │ │ ├── auth.service.ts │ │ ├── book.service.ts │ │ └── index.ts │ │ └── state │ │ ├── __snapshots__ │ │ └── movie.reducer.spec.ts.snap │ │ ├── auth.reducer.ts │ │ ├── books.reducer.ts │ │ ├── index.ts │ │ └── logout.metareducer.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── setupJest.ts ├── styles.css ├── tsconfig.app.json └── tsconfig.spec.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /AUTH_CODE_LAB.md: -------------------------------------------------------------------------------- 1 | # Auth Module Challenge 2 | 3 | In this extended challenge you are being asked to implement the NgRx State portion of a new authentication module. The auth system you are integrating with is pretty straightforward: 4 | 5 | 1. When the application launches reach out to the API via the AuthService to check and see if you are authenticated 6 | 2. When the user attempts to login, use the `login` method on the AuthService to attempt to login supplying a username and password. Note that this service call MAY FAIL and you should attempt to handle errors for it 7 | 3. When the user logs out simply call the synchronous `logout` method on the AuthService 8 | 9 | ## Step One: User Actions 10 | 11 | There are really only two ways the user can interact with the authentication module: they can login via the login page or they can logout by pressing the logout icon button in the navigation drawer. With this in mind, the first part of the challenge is to define some **actions** that model all of the ways the user can interact with the authentication module. 12 | 13 | In the previous sections of this workshop we've used the `props<>()` utility function to define additional properties on actions. For this section, try out this alternative syntax: 14 | 15 | ```ts 16 | export const createBook = createAction( 17 | "[Books Page] Create Book", 18 | (book: BookRequiredProps) => ({ book }) 19 | ); 20 | ``` 21 | 22 | This syntax allows you to customize the function signature of the action creator that is returned from `createAction`. 23 | 24 | ### Challenge: Auth User Actions 25 | 26 | **PR: 12-auth-user-actions** 27 | 28 | 1. Open **auth-user.actions.ts** and define actions for when the user **logs in** and **logs out**. What additional data should you put on these actions to give them context? 29 | 2. Open **login-page.component.ts** in the Auth module and have it dispatch your new **login** action in the `onLogin` method. 30 | 3. Open **user.component.ts** in the Auth module and have it dispatch your new **logout** action in the `onLogout` method. 31 | 32 | ## Step Two: Auth State 33 | 34 | The components your team has implemented for the Auth module care about three pieces of state: who is the currently authenticated user? Am I currently retrieving the authentication status? And was there an error when the user attempted to login? 35 | 36 | With this in mind your next challenge is to define an auth reducer that can manage these three pieces of state. It should also be setup to hand the new `logout` and `login` actions you created in the previous step. 37 | 38 | ### Challenge: Auth State 39 | 40 | **PR: 13-auth-state** 41 | 42 | 1. Open **auth.reducer.ts** in the State module and define an interface called `State` with properties for `gettingStatus`, `user`, and `error`. 43 | 2. Define a constant called `initialState` that implements the `State` interface. What should these properties of state be initialized to when the application launches? 44 | 3. Define a reducer called `authReducer` using the `createReducer` helper, passing `initialState` in as the first argument 45 | 4. Add a state transition function that handles the `logout` action. When the user logs out what should state look like? 46 | 5. Add a state transition function that handles the `login` action. When the user starts to authenticate what should state look like÷ 47 | 6. Define a statically analyzable function simply called `reducer` that wraps `authReducer` to make it AOT-compatible 48 | 49 | ## Step Three: Auth Selectors 50 | 51 | Now that you have a reducer defined it is time to author some selectors to read the state it manages and then connect the reducer to the Store so that it starts receiving actions. 52 | 53 | ### Challenge: Auth Selectors 54 | 55 | **PR: 14-auth-selectors** 56 | 57 | 1. Open **auth.reducer.ts** and define three **getter selectors** for the `gettingStatus`, `user`, and `error` properties 58 | 2. Open `state/index.ts` and register the auth reducer's `State` interface and `reducer` function in the global `State` interface and the global `reducers` object respectively 59 | 3. Define a **getter selector** for selecting the **auth state** 60 | 4. Use the **auth state selector** and `createSelector` to export the three selectors you authored in step 1 61 | 62 | ## Step Four: Reading Auth State 63 | 64 | There are two components in the Auth module that are expecting to be able to read in authentication state from the store. 65 | 66 | The first is the `login-page` component. It uses an `*ngIf` directive on the top level `` to prevent any route from being displayed until the user has authenticated. While the user is authenticating or while the app is retrieving authentication status it shows a spinner. If the app is not actively authenticating and there is not a user authenticated it shows the login form. Finally, if the user is not authenticated and has failed to authenticate it passes down an error message to the login form. 67 | 68 | The second component is the `user` component. It shows the username of the currently authenticated user and a logout button. 69 | 70 | In this next challenge you'll need to use the selectors you've authored to connect these two components to the Store. 71 | 72 | ### Challenge: Reading Auth State 73 | 74 | **PR: 15-reading-auth-state** 75 | 76 | 1. Open `login-page.component.ts` in the Auth module and remove the mock data observables being assigned to the `gettingStatus$`, `user$`, and `error$` properties. 77 | 2. Use the selectors you authored in the previous challenge to initialize these properties in the component's constructor. 78 | 3. Open `user.component.ts` in the Auth module and remove the mock data observable being assigned to the `user$` property. 79 | 4. Use the selectors you authored in the previous challenge to initialize the `user$` property. 80 | 81 | ## Step Five: Auth API Actions 82 | 83 | At this point your application should be showing the login page. If you attempt to authenticate it will perpetually show a spinner. That's because we haven't written actions for the Auth API and there aren't any effects to dispatch them. 84 | 85 | The first auth API you will have to interact with is `getStatus()`. For the purpose of this exercise assume it never fails. It will return to you either a `UserModel` if there is an authenticated user or `null` if there is not an authenticated user. 86 | 87 | The second API you will interact with is `login()`. This method requires a username and password. If the username and password are correct the observable will return the `UserModel` for the newly authenticated user. If the username or password is incorrect the service with throw an error. The error will be a string and describes the reason why authentication failed. 88 | 89 | The third API you will interact with is `logout()`. It is completely synchronous and does not return an observable. It will always succeed and logs the user out immediately. 90 | 91 | ### Challenge: Auth API Actions 92 | 93 | **PR: 16-auth-api-actions** 94 | 95 | 1. Open `auth-api.actions.ts` in the Auth module 96 | 2. Define actions that capture all of the unique events `getStatus()` could emit (is there more than one?) 97 | 3. Define actions that capture all of the unique events that `login()` could emit (should be two: success and failure) 98 | 4. Define any actions that are needed to capture the unique events that `logout()` could emit (are there any?) 99 | 100 | ## Step Six: Auth Effects 101 | 102 | In the previous challenge you should have authored a total of three actions: `getStatusSuccess`, `loginSuccess`, and `loginFailure`. If you didn't get that right review the PR and adjust your application as needed. 103 | 104 | The idea here is to pair one action for each emmission type of the observables that are returned by the service. Since `getStatus` can only succeed we write one action for it. Since `login` can succeed or fail we write an action for each case. Finally, `logout` does not emit anything at all and just happens. There isn't a need to model this with an action. 105 | 106 | Now that the actions have been authored it is time to write effects that interact with the `AuthService`. Keep in mind a few things from our previous lesson advanced effects: not all effects need to start with the `Actions` service, you can use `catchError` with `of` to map errors into actions, and not all effects need to dispatch actions. 107 | 108 | ### Challenge: Auth Effects 109 | 110 | **PR: 17-auth-effects** 111 | 112 | 1. Open `auth.effects.ts` in the Auth module 113 | 2. Create an effect class called `AuthEffects` and apply the `@Injectable()` decorator to it. 114 | 3. Inject the `Actions` service and `AuthService` into the effects class 115 | 4. Define an effect called `getAuthStatus$` that immediately gets the authentication status on bootup and dispatches the appropriate action when complete 116 | 5. Define an effect called `login$` that calls the `login()` method when the user logs in. Be sure to map its success and error emmissions into actions. 117 | 6. Define an effect called `logout$` that simply calls the `logout()` method when the user logs out. This effect doesn't need to dispatch any actions. 118 | 7. Open `auth.module.ts` and register your new effect in the `imports` of the `AuthModule` using `EffectsModule.forFeature([...])` 119 | 120 | ## Step Seven: Handling Auth API Actions 121 | 122 | Now that effects are running and are dispatching actions for the API it is time to update our auth reducer to handle these actions. 123 | 124 | ### Challenge: Handling Auth API Actions 125 | 126 | **PR: 18-handling-auth-api-actions** 127 | 128 | 1. Open `auth.reducer.ts` in the State module 129 | 2. Add a state transition function that handles the `getAuthStatusSuccess` action. How should state change when this action is handled? 130 | 3. Add a state transition function that handles the `loginSuccess` action. How should state change when this action is handled? 131 | 4. Add a state transition function that handles the `loginFailure` action. How should state change when this action is handled? 132 | 133 | ## Step Eight: Resetting State on Logout 134 | 135 | By this point the app should run and you should be able to authenticate successfully, logout successfully, and even show errors if you fail to authenticate. There's just one problem though: if you use the Redux Devtools you'll see that when you logout all of the books are kept in state! This could be sensitive information so you'll need to reset all of this state when the user logs out. 136 | 137 | To do this, you are going to write a meta-reducer that handles the `logout` action. When a `logout` action is dispatched your metareducer can send `undefined` to all of your other reducers to reset their state. All other actions should be processed as normal. 138 | 139 | ### Challenge: Logout Meta-Reducer 140 | 141 | **PR: 19-logout-metareducer** 142 | 143 | 1. Open `logout.metareducer.ts` and implement a meta-reducer that resets all of state when a `logout` action is dispatched 144 | 2. Open `state/index.ts` and register the meta-reducer in the `metaReducers` array 145 | 146 | Now when you logout the state should be completely reset! Push your code to your personal fork and let us know that you've completed this code lab. 147 | -------------------------------------------------------------------------------- /NgRxWorkshop.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSequence/ngrx-workshop-ngconf2020/9489a335b2f1a5f9f63f248ebe2818649ae30368/NgRxWorkshop.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgRx Workshop - ng-conf 2020 2 | 3 | ## Setup 4 | To get started, ensure that you have *at least* Node v10 installed. You can verify what version you have installed by running 5 | ```sh 6 | node --version 7 | ``` 8 | from your terminal. Then, fork this repository to your personal Github account. As you make changes to the codebase during this workshop you will be pushing your changes to your personal fork. You can [learn more about forks here](https://help.github.com/en/github/getting-started-with-github/fork-a-repo). 9 | 10 | Once you have forked this repository, clone your fork to your development machine. Then run the following command using your terminal from the root of the repository to install dependencies. 11 | 12 | ```sh 13 | npm install 14 | ``` 15 | 16 | or 17 | 18 | ```sh 19 | yarn 20 | ``` 21 | 22 | With the dependencies installed run the app to verify everything is working correctly. 23 | 24 | ## Running the app 25 | To run the app execute the following command from your terminal: 26 | ```sh 27 | npm start 28 | ``` 29 | or 30 | 31 | ```sh 32 | yarn start 33 | ``` 34 | 35 | The app should now be running at [http://localhost:4200](http://localhost:4200) 36 | 37 | 38 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngrx-workshop-example": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "ngrx", 11 | "schematics": { 12 | "@schematics/angular:class": { 13 | "skipTests": true 14 | }, 15 | "@schematics/angular:component": { 16 | "skipTests": true 17 | }, 18 | "@schematics/angular:directive": { 19 | "skipTests": true 20 | }, 21 | "@schematics/angular:guard": { 22 | "skipTests": true 23 | }, 24 | "@schematics/angular:module": { 25 | "skipTests": true 26 | }, 27 | "@schematics/angular:pipe": { 28 | "skipTests": true 29 | }, 30 | "@schematics/angular:service": { 31 | "skipTests": true 32 | } 33 | }, 34 | "architect": { 35 | "build": { 36 | "builder": "@angular-devkit/build-angular:browser", 37 | "options": { 38 | "aot": true, 39 | "outputPath": "dist/ngrx-workshop-example", 40 | "index": "src/index.html", 41 | "main": "src/main.ts", 42 | "polyfills": "src/polyfills.ts", 43 | "tsConfig": "src/tsconfig.app.json", 44 | "assets": [ 45 | "src/favicon.ico", 46 | "src/assets" 47 | ], 48 | "styles": [ 49 | "src/styles.css" 50 | ], 51 | "scripts": [], 52 | "es5BrowserSupport": true 53 | }, 54 | "configurations": { 55 | "production": { 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.prod.ts" 60 | } 61 | ], 62 | "optimization": true, 63 | "outputHashing": "all", 64 | "sourceMap": false, 65 | "extractCss": true, 66 | "namedChunks": false, 67 | "aot": true, 68 | "extractLicenses": true, 69 | "vendorChunk": false, 70 | "buildOptimizer": true, 71 | "budgets": [ 72 | { 73 | "type": "initial", 74 | "maximumWarning": "2mb", 75 | "maximumError": "5mb" 76 | }, 77 | { 78 | "type": "anyComponentStyle", 79 | "maximumWarning": "6kb" 80 | } 81 | ] 82 | } 83 | } 84 | }, 85 | "serve": { 86 | "builder": "@angular-devkit/build-angular:dev-server", 87 | "options": { 88 | "browserTarget": "ngrx-workshop-example:build" 89 | }, 90 | "configurations": { 91 | "production": { 92 | "browserTarget": "ngrx-workshop-example:build:production" 93 | } 94 | } 95 | }, 96 | "extract-i18n": { 97 | "builder": "@angular-devkit/build-angular:extract-i18n", 98 | "options": { 99 | "browserTarget": "ngrx-workshop-example:build" 100 | } 101 | }, 102 | "test": { 103 | "builder": "@angular-devkit/build-angular:karma", 104 | "options": { 105 | "main": "src/test.ts", 106 | "polyfills": "src/polyfills.ts", 107 | "tsConfig": "src/tsconfig.spec.json", 108 | "karmaConfig": "src/karma.conf.js", 109 | "styles": [ 110 | "src/styles.css" 111 | ], 112 | "scripts": [], 113 | "assets": [ 114 | "src/favicon.ico", 115 | "src/assets" 116 | ] 117 | } 118 | }, 119 | "lint": { 120 | "builder": "@angular-devkit/build-angular:tslint", 121 | "options": { 122 | "tsConfig": [ 123 | "src/tsconfig.app.json", 124 | "src/tsconfig.spec.json" 125 | ], 126 | "exclude": [ 127 | "**/node_modules/**" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "ngrx-workshop-example" 135 | } -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "books": [ 3 | { 4 | "id": "ca66d740-e41d-4b70-aea1-a8a20759a647", 5 | "name": "The Lion, the Witch and the Wardrobe", 6 | "earnings": "85000000", 7 | "description": "" 8 | }, 9 | { 10 | "id": "408c6851-7b7a-439e-b6db-9fb65ea61071", 11 | "name": "The Adventures of Pinocchio", 12 | "earnings": "85000000", 13 | "description": "" 14 | }, 15 | { 16 | "id": "3fe92243-8444-4ee4-bb39-b5460b8919f1", 17 | "name": "The Hobbit", 18 | "earnings": "100000000", 19 | "description": null 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-workshop-example", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "concurrently \"npm run server\" \"ng serve\"", 7 | "server": "json-server db.json --watch", 8 | "build": "ng build", 9 | "test": "jest", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "jest": { 14 | "preset": "jest-preset-angular", 15 | "setupFilesAfterEnv": [ 16 | "/src/setupJest.ts" 17 | ] 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "~9.1.0", 22 | "@angular/cdk": "~9.2.0", 23 | "@angular/common": "~9.1.0", 24 | "@angular/compiler": "~9.1.0", 25 | "@angular/core": "~9.1.0", 26 | "@angular/forms": "~9.1.0", 27 | "@angular/material": "~9.2.0", 28 | "@angular/platform-browser": "~9.1.0", 29 | "@angular/platform-browser-dynamic": "~9.1.0", 30 | "@angular/router": "~9.1.0", 31 | "@ngrx/effects": "^9.0.0", 32 | "@ngrx/entity": "^9.0.0", 33 | "@ngrx/router-store": "^9.0.0", 34 | "@ngrx/store": "^9.0.0", 35 | "@ngrx/store-devtools": "^9.0.0", 36 | "core-js": "^2.5.4", 37 | "rxjs": "~6.5.4", 38 | "tslib": "^1.10.0", 39 | "uuid": "^3.3.3", 40 | "zone.js": "~0.10.2" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "~0.901.0", 44 | "@angular/cli": "~9.1.0", 45 | "@angular/compiler-cli": "~9.1.0", 46 | "@angular/language-service": "~9.1.0", 47 | "@types/jest": "^24.0.18", 48 | "@types/node": "^12.11.1", 49 | "@types/uuid": "^3.4.5", 50 | "codelyzer": "^5.1.2", 51 | "concurrently": "^4.1.2", 52 | "jasmine-marbles": "^0.6.0", 53 | "jest": "^24.9.0", 54 | "jest-preset-angular": "^7.1.1", 55 | "json-server": "^0.15.1", 56 | "ts-node": "~8.3.0", 57 | "tslint": "~5.19.0", 58 | "typescript": "~3.8.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | } 6 | 7 | .nav-link { 8 | color: rgba(0, 0, 0, 0.54); 9 | display: flex; 10 | align-items: center; 11 | padding-top: 5px; 12 | padding-bottom: 5px; 13 | } 14 | 15 | mat-toolbar { 16 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 17 | 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); 18 | z-index: 1; 19 | } 20 | 21 | mat-toolbar > .mat-mini-fab { 22 | margin-right: 10px; 23 | } 24 | 25 | mat-sidenav { 26 | box-shadow: 3px 0 6px rgba(0, 0, 0, 0.24); 27 | width: 200px; 28 | } 29 | 30 | .mat-sidenav-container { 31 | background: #f5f5f5; 32 | flex: 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 25 | 26 | 27 |
28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-root", 5 | templateUrl: "./app.component.html", 6 | styleUrls: ["./app.component.css"] 7 | }) 8 | export class AppComponent { 9 | title = "NgRx Workshop"; 10 | links = [{ path: "/books", icon: "book", label: "Books" }]; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from "@angular/platform-browser"; 2 | import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; 3 | import { NgModule } from "@angular/core"; 4 | import { RouterModule } from "@angular/router"; 5 | import { HttpClientModule } from "@angular/common/http"; 6 | import { StoreModule } from "@ngrx/store"; 7 | import { StoreDevtoolsModule } from "@ngrx/store-devtools"; 8 | import { EffectsModule } from "@ngrx/effects"; 9 | import { MaterialModule } from "./material.module"; 10 | import { reducers, metaReducers } from "./shared/state"; 11 | import { AuthModule } from "./auth"; 12 | import { BooksModule } from "./books"; 13 | import { AppComponent } from "./app.component"; 14 | 15 | @NgModule({ 16 | declarations: [AppComponent], 17 | imports: [ 18 | BrowserModule, 19 | BrowserAnimationsModule, 20 | HttpClientModule, 21 | RouterModule.forRoot([ 22 | { path: "", pathMatch: "full", redirectTo: "/books" } 23 | ]), 24 | StoreModule.forRoot(reducers, { metaReducers }), 25 | StoreDevtoolsModule.instrument(), 26 | EffectsModule.forRoot([]), 27 | MaterialModule, 28 | AuthModule, 29 | BooksModule 30 | ], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule {} 34 | -------------------------------------------------------------------------------- /src/app/auth/actions/auth-api.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "@ngrx/store"; 2 | import { UserModel } from "src/app/shared/models"; 3 | -------------------------------------------------------------------------------- /src/app/auth/actions/auth-user.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction } from "@ngrx/store"; 2 | -------------------------------------------------------------------------------- /src/app/auth/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as AuthApiActions from "./auth-api.actions"; 2 | import * as AuthUserActions from "./auth-user.actions"; 3 | 4 | export { AuthApiActions, AuthUserActions }; 5 | -------------------------------------------------------------------------------- /src/app/auth/auth.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { createEffect, Actions, ofType } from "@ngrx/effects"; 3 | import {} from "rxjs"; 4 | import {} from "rxjs/operators"; 5 | import { AuthService } from "../shared/services/auth.service"; 6 | import { AuthApiActions, AuthUserActions } from "./actions"; 7 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { EffectsModule } from "@ngrx/effects"; 3 | import { LoginPageComponentModule } from "./components/login-page"; 4 | import { UserComponentModule } from "./components/user"; 5 | 6 | @NgModule({ 7 | exports: [LoginPageComponentModule, UserComponentModule] 8 | }) 9 | export class AuthModule {} 10 | -------------------------------------------------------------------------------- /src/app/auth/components/login-form/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./login-form.component"; 2 | export * from "./login-form.module"; 3 | -------------------------------------------------------------------------------- /src/app/auth/components/login-form/login-form.component.css: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | width: 300px; 4 | flex-direction: column; 5 | padding: 12px 20px 20px; 6 | margin: 80px auto; 7 | background-color: white; 8 | border-radius: 2px; 9 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.12); 10 | } 11 | 12 | p { 13 | padding: 0; 14 | margin: 8px 0; 15 | font-style: italic; 16 | opacity: 0.82; 17 | } 18 | 19 | span { 20 | background-color: #f44336; 21 | padding: 8px; 22 | border-radius: 2px; 23 | margin: 12px 0 0; 24 | color: black; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/auth/components/login-form/login-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Login

3 |

The username can be anything and the password must be "password"

4 | 5 | 11 | 12 | 13 | 19 | 20 |
21 | 24 |
25 | {{ error }} 26 |
27 | -------------------------------------------------------------------------------- /src/app/auth/components/login-form/login-form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, Input } from "@angular/core"; 2 | import { FormGroup, FormControl } from "@angular/forms"; 3 | 4 | export interface LoginEvent { 5 | username: string; 6 | password: string; 7 | } 8 | 9 | @Component({ 10 | selector: "app-login-form", 11 | templateUrl: "./login-form.component.html", 12 | styleUrls: ["./login-form.component.css"] 13 | }) 14 | export class LoginFormComponent { 15 | @Output() login = new EventEmitter(); 16 | @Input() error: string | null = null; 17 | 18 | formGroup = new FormGroup({ 19 | username: new FormControl(""), 20 | password: new FormControl("") 21 | }); 22 | 23 | onSubmit() { 24 | this.login.emit(this.formGroup.value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/components/login-form/login-form.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { ReactiveFormsModule } from "@angular/forms"; 3 | import { CommonModule } from "@angular/common"; 4 | import { MatInputModule } from "@angular/material/input"; 5 | import { MatButtonModule } from "@angular/material/button"; 6 | import { LoginFormComponent } from "./login-form.component"; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, MatInputModule, MatButtonModule, ReactiveFormsModule], 10 | declarations: [LoginFormComponent], 11 | exports: [LoginFormComponent] 12 | }) 13 | export class LoginFormComponentModule {} 14 | -------------------------------------------------------------------------------- /src/app/auth/components/login-page/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./login-page.component"; 2 | export * from "./login-page.module"; 3 | -------------------------------------------------------------------------------- /src/app/auth/components/login-page/login-page.component.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | display: block; 3 | width: 40px; 4 | margin: 80px auto; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/auth/components/login-page/login-page.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/app/auth/components/login-page/login-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { Observable, of } from "rxjs"; 3 | import { Store } from "@ngrx/store"; 4 | import { UserModel } from "src/app/shared/models"; 5 | import { AuthUserActions } from "../../actions"; 6 | import { LoginEvent } from "../login-form"; 7 | 8 | @Component({ 9 | selector: "app-login-page", 10 | templateUrl: "./login-page.component.html", 11 | styleUrls: ["./login-page.component.css"] 12 | }) 13 | export class LoginPageComponent { 14 | gettingStatus$: Observable = of(false); 15 | user$: Observable = of({ 16 | id: "1", 17 | username: "NgRx Learner" 18 | }); 19 | error$: Observable = of(null); 20 | 21 | onLogin($event: LoginEvent) { 22 | // Not Implemented 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/auth/components/login-page/login-page.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { RouterModule } from "@angular/router"; 4 | import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; 5 | import { LoginFormComponentModule } from "../login-form"; 6 | import { LoginPageComponent } from "./login-page.component"; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | RouterModule, 12 | MatProgressSpinnerModule, 13 | LoginFormComponentModule 14 | ], 15 | declarations: [LoginPageComponent], 16 | exports: [LoginPageComponent] 17 | }) 18 | export class LoginPageComponentModule {} 19 | -------------------------------------------------------------------------------- /src/app/auth/components/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user.component"; 2 | export * from "./user.module"; 3 | -------------------------------------------------------------------------------- /src/app/auth/components/user/user.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | padding: 12px 0 12px 12px; 4 | grid-template-areas: 5 | "welcome logout" 6 | "name logout"; 7 | grid-template-columns: 1fr 40px; 8 | } 9 | 10 | h4 { 11 | grid-area: welcome; 12 | margin: 0; 13 | font-weight: normal; 14 | } 15 | 16 | h3 { 17 | grid-area: name; 18 | margin: 0; 19 | } 20 | 21 | button { 22 | grid-area: logout; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/auth/components/user/user.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Welcome

3 |

{{ user.username }}

4 | 12 |
13 | -------------------------------------------------------------------------------- /src/app/auth/components/user/user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { Observable, of } from "rxjs"; 3 | import { UserModel } from "src/app/shared/models"; 4 | import { Store } from "@ngrx/store"; 5 | import { State } from "src/app/shared/state"; 6 | import { AuthUserActions } from "../../actions"; 7 | 8 | @Component({ 9 | selector: "app-user", 10 | templateUrl: "./user.component.html", 11 | styleUrls: ["./user.component.css"] 12 | }) 13 | export class UserComponent { 14 | user$: Observable = of({ 15 | id: "1", 16 | username: "NgRx Learner" 17 | }); 18 | 19 | onLogout() { 20 | // Not Implemented 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/auth/components/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { MatButtonModule } from "@angular/material/button"; 4 | import { MatIconModule } from "@angular/material/icon"; 5 | import { MatTooltipModule } from "@angular/material/tooltip"; 6 | import { UserComponent } from "./user.component"; 7 | 8 | @NgModule({ 9 | imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule], 10 | declarations: [UserComponent], 11 | exports: [UserComponent] 12 | }) 13 | export class UserComponentModule {} 14 | -------------------------------------------------------------------------------- /src/app/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth.module"; 2 | -------------------------------------------------------------------------------- /src/app/books/actions/books-api.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from "@ngrx/store"; 2 | import { BookModel } from "src/app/shared/models"; 3 | -------------------------------------------------------------------------------- /src/app/books/actions/books-page.actions.ts: -------------------------------------------------------------------------------- 1 | import { createAction, props } from "@ngrx/store"; 2 | import { BookRequiredProps } from "src/app/shared/models"; 3 | -------------------------------------------------------------------------------- /src/app/books/actions/index.ts: -------------------------------------------------------------------------------- 1 | import * as BooksPageActions from "./books-page.actions"; 2 | import * as BooksApiActions from "./books-api.actions"; 3 | 4 | export { BooksPageActions, BooksApiActions }; 5 | -------------------------------------------------------------------------------- /src/app/books/books-api.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { createEffect, Actions, ofType } from "@ngrx/effects"; 3 | import {} from "rxjs/operators"; 4 | import { BooksService } from "../shared/services"; 5 | import { BooksPageActions, BooksApiActions } from "./actions"; 6 | -------------------------------------------------------------------------------- /src/app/books/books.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { CommonModule } from "@angular/common"; 3 | import { RouterModule } from "@angular/router"; 4 | import { ReactiveFormsModule } from "@angular/forms"; 5 | import { EffectsModule } from "@ngrx/effects"; 6 | import { MaterialModule } from "src/app/material.module"; 7 | import { BooksPageComponent } from "./components/books-page/books-page.component"; 8 | import { BookDetailComponent } from "./components/book-detail/book-detail.component"; 9 | import { BooksListComponent } from "./components/books-list/books-list.component"; 10 | import { BooksTotalComponent } from "./components/books-total/books-total.component"; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | ReactiveFormsModule, 16 | MaterialModule, 17 | RouterModule.forChild([{ path: "books", component: BooksPageComponent }]) 18 | ], 19 | declarations: [ 20 | BooksPageComponent, 21 | BookDetailComponent, 22 | BooksListComponent, 23 | BooksTotalComponent 24 | ] 25 | }) 26 | export class BooksModule {} 27 | -------------------------------------------------------------------------------- /src/app/books/components/book-detail/book-detail.component.css: -------------------------------------------------------------------------------- 1 | mat-card-actions { 2 | margin-bottom: 0; 3 | } 4 | mat-card-header { 5 | margin-bottom: 10px; 6 | } 7 | .full-width { 8 | width: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/books/components/book-detail/book-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Editing {{ originalBook.name }} 8 | Create Book 9 |

10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /src/app/books/components/book-detail/book-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from "@angular/core"; 2 | import { FormGroup, FormControl } from "@angular/forms"; 3 | import { BookModel } from "src/app/shared/models"; 4 | 5 | @Component({ 6 | selector: "app-book-detail", 7 | templateUrl: "./book-detail.component.html", 8 | styleUrls: ["./book-detail.component.css"] 9 | }) 10 | export class BookDetailComponent { 11 | originalBook: BookModel | undefined; 12 | @Output() save = new EventEmitter(); 13 | @Output() cancel = new EventEmitter(); 14 | 15 | bookForm = new FormGroup({ 16 | name: new FormControl(""), 17 | earnings: new FormControl(0), 18 | description: new FormControl("") 19 | }); 20 | 21 | @Input() set book(book: BookModel) { 22 | this.bookForm.reset(); 23 | this.originalBook = undefined; 24 | 25 | if (book) { 26 | this.bookForm.setValue({ 27 | name: book.name, 28 | earnings: book.earnings, 29 | description: book.description 30 | }); 31 | 32 | this.originalBook = book; 33 | } 34 | } 35 | 36 | onSubmit(book: BookModel) { 37 | this.save.emit({ ...this.originalBook, ...book }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/books/components/books-list/books-list.component.css: -------------------------------------------------------------------------------- 1 | mat-list-item:not(:first-of-type) { 2 | border-top: 1px solid #efefef; 3 | } 4 | 5 | .symbol { 6 | color: #777; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/books/components/books-list/books-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Books

5 |
6 |
7 | 8 | 9 | 14 |

{{ book.name }}

15 |

16 | {{ book.description }} 17 |

18 |

19 | {{ book.earnings | currency }} 20 |

21 | 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /src/app/books/components/books-list/books-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from "@angular/core"; 2 | import { BookModel } from "src/app/shared/models"; 3 | 4 | @Component({ 5 | selector: "app-books-list", 6 | templateUrl: "./books-list.component.html", 7 | styleUrls: ["./books-list.component.css"] 8 | }) 9 | export class BooksListComponent { 10 | @Input() books: BookModel[]; 11 | @Input() readonly = false; 12 | @Output() select = new EventEmitter(); 13 | @Output() delete = new EventEmitter(); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/books/components/books-page/books-page.component.css: -------------------------------------------------------------------------------- 1 | :host >>> mat-list-item:hover { 2 | cursor: pointer; 3 | background: whitesmoke; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/books/components/books-page/books-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 10 | 11 |
12 | 13 | 19 | 20 |
21 | -------------------------------------------------------------------------------- /src/app/books/components/books-page/books-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { 3 | BookModel, 4 | calculateBooksGrossEarnings, 5 | BookRequiredProps 6 | } from "src/app/shared/models"; 7 | import { BooksService } from "src/app/shared/services"; 8 | 9 | @Component({ 10 | selector: "app-books", 11 | templateUrl: "./books-page.component.html", 12 | styleUrls: ["./books-page.component.css"] 13 | }) 14 | export class BooksPageComponent implements OnInit { 15 | books: BookModel[] = []; 16 | currentBook: BookModel | null = null; 17 | total: number = 0; 18 | 19 | constructor(private booksService: BooksService) {} 20 | 21 | ngOnInit() { 22 | this.getBooks(); 23 | this.removeSelectedBook(); 24 | } 25 | 26 | getBooks() { 27 | this.booksService.all().subscribe(books => { 28 | this.books = books; 29 | this.updateTotals(books); 30 | }); 31 | } 32 | 33 | updateTotals(books: BookModel[]) { 34 | this.total = calculateBooksGrossEarnings(books); 35 | } 36 | 37 | onSelect(book: BookModel) { 38 | this.currentBook = book; 39 | } 40 | 41 | onCancel() { 42 | this.removeSelectedBook(); 43 | } 44 | 45 | removeSelectedBook() { 46 | this.currentBook = null; 47 | } 48 | 49 | onSave(book: BookRequiredProps | BookModel) { 50 | if ("id" in book) { 51 | this.updateBook(book); 52 | } else { 53 | this.saveBook(book); 54 | } 55 | } 56 | 57 | saveBook(bookProps: BookRequiredProps) { 58 | this.booksService.create(bookProps).subscribe(() => { 59 | this.getBooks(); 60 | this.removeSelectedBook(); 61 | }); 62 | } 63 | 64 | updateBook(book: BookModel) { 65 | this.booksService.update(book.id, book).subscribe(() => { 66 | this.getBooks(); 67 | this.removeSelectedBook(); 68 | }); 69 | } 70 | 71 | onDelete(book: BookModel) { 72 | this.booksService.delete(book.id).subscribe(() => { 73 | this.getBooks(); 74 | this.removeSelectedBook(); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/books/components/books-total/books-total.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSequence/ngrx-workshop-ngconf2020/9489a335b2f1a5f9f63f248ebe2818649ae30368/src/app/books/components/books-total/books-total.component.css -------------------------------------------------------------------------------- /src/app/books/components/books-total/books-total.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Books Gross Total

5 |
6 |
7 | 8 | {{ total | currency }} 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/books/components/books-total/books-total.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "app-books-total", 5 | templateUrl: "./books-total.component.html", 6 | styleUrls: ["./books-total.component.css"] 7 | }) 8 | export class BooksTotalComponent { 9 | @Input() total: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/books/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./books.module"; 2 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { MatButtonModule } from "@angular/material/button"; 3 | import { MatCardModule } from "@angular/material/card"; 4 | import { MatCheckboxModule } from "@angular/material/checkbox"; 5 | import { MatIconModule } from "@angular/material/icon"; 6 | import { MatInputModule } from "@angular/material/input"; 7 | import { MatListModule } from "@angular/material/list"; 8 | import { MatSidenavModule } from "@angular/material/sidenav"; 9 | import { MatToolbarModule } from "@angular/material/toolbar"; 10 | 11 | @NgModule({ 12 | exports: [ 13 | MatButtonModule, 14 | MatCardModule, 15 | MatCheckboxModule, 16 | MatIconModule, 17 | MatInputModule, 18 | MatListModule, 19 | MatSidenavModule, 20 | MatToolbarModule 21 | ] 22 | }) 23 | export class MaterialModule {} 24 | -------------------------------------------------------------------------------- /src/app/shared/models/book.model.ts: -------------------------------------------------------------------------------- 1 | export interface BookModel { 2 | id: string; 3 | name: string; 4 | earnings: number; 5 | description?: string; 6 | } 7 | 8 | export type BookRequiredProps = Pick; 9 | 10 | export function calculateBooksGrossEarnings(books: BookModel[]) { 11 | return books.reduce((total, book) => { 12 | return total + parseInt(`${book.earnings}`, 10) || 0; 13 | }, 0); 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./book.model"; 2 | export * from "./user.model"; 3 | -------------------------------------------------------------------------------- /src/app/shared/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserModel { 2 | id: string; 3 | username: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/shared/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { timer, Observable, throwError } from "rxjs"; 3 | import { map } from "rxjs/operators"; 4 | import * as uuid from "uuid/v4"; 5 | import { UserModel } from "../models/user.model"; 6 | 7 | @Injectable({ 8 | providedIn: "root" 9 | }) 10 | export class AuthService { 11 | login(username: string, password: string) { 12 | if (password !== "password" || !username.trim()) { 13 | return throwError(new Error("Invalid username or password")); 14 | } 15 | 16 | return timer(750).pipe( 17 | map(() => { 18 | const user = { id: uuid(), username }; 19 | 20 | localStorage.setItem("auth", JSON.stringify(user)); 21 | 22 | return user; 23 | }) 24 | ); 25 | } 26 | 27 | getStatus(): Observable { 28 | return timer(750).pipe( 29 | map(() => { 30 | const userString = localStorage.getItem("auth"); 31 | 32 | if (!userString) return null; 33 | 34 | return JSON.parse(userString); 35 | }) 36 | ); 37 | } 38 | 39 | logout() { 40 | localStorage.removeItem("auth"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/services/book.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpHeaders } from "@angular/common/http"; 2 | import { Injectable } from "@angular/core"; 3 | import * as uuid from "uuid/v4"; 4 | import { BookModel, BookRequiredProps } from "../models"; 5 | 6 | const BASE_URL = "http://localhost:3000/books"; 7 | const HEADER = { 8 | headers: new HttpHeaders({ "Content-Type": "application/json" }) 9 | }; 10 | 11 | @Injectable({ 12 | providedIn: "root" 13 | }) 14 | export class BooksService { 15 | constructor(private http: HttpClient) {} 16 | 17 | all() { 18 | return this.http.get(BASE_URL); 19 | } 20 | 21 | load(id: string) { 22 | return this.http.get(`${BASE_URL}/${id}`); 23 | } 24 | 25 | create(bookProps: BookRequiredProps) { 26 | const Book: BookModel = { 27 | id: uuid(), 28 | ...bookProps 29 | }; 30 | 31 | return this.http.post( 32 | `${BASE_URL}`, 33 | JSON.stringify(Book), 34 | HEADER 35 | ); 36 | } 37 | 38 | update(id: string, updates: BookRequiredProps) { 39 | return this.http.patch( 40 | `${BASE_URL}/${id}`, 41 | JSON.stringify(updates), 42 | HEADER 43 | ); 44 | } 45 | 46 | delete(id: string) { 47 | return this.http.delete(`${BASE_URL}/${id}`); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/shared/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth.service"; 2 | export * from "./book.service"; 3 | -------------------------------------------------------------------------------- /src/app/shared/state/__snapshots__/movie.reducer.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Movie Reducer should add newly created movies to the state 1`] = ` 4 | Object { 5 | "activeMovieId": "1", 6 | "entities": Object { 7 | "1": Object { 8 | "earnings": 100000, 9 | "id": "1", 10 | "name": "Arrival", 11 | }, 12 | }, 13 | "ids": Array [ 14 | "1", 15 | ], 16 | } 17 | `; 18 | 19 | exports[`Movie Reducer should apply changes to a movie when a movie is updated 1`] = ` 20 | Object { 21 | "activeMovieId": "1", 22 | "entities": Object { 23 | "1": Object { 24 | "earnings": 120000, 25 | "id": "1", 26 | "name": "Blade Runner", 27 | }, 28 | }, 29 | "ids": Array [ 30 | "1", 31 | ], 32 | } 33 | `; 34 | 35 | exports[`Movie Reducer should load all movies when the API loads them all successfully 1`] = ` 36 | Object { 37 | "activeMovieId": null, 38 | "entities": Object { 39 | "1": Object { 40 | "earnings": 0, 41 | "id": "1", 42 | "name": "Green Lantern", 43 | }, 44 | }, 45 | "ids": Array [ 46 | "1", 47 | ], 48 | } 49 | `; 50 | 51 | exports[`Movie Reducer should remove movies from the state when they are deleted 1`] = ` 52 | Object { 53 | "activeMovieId": "1", 54 | "entities": Object { 55 | "1": Object { 56 | "earnings": 1000, 57 | "id": "1", 58 | "name": "mother!", 59 | }, 60 | }, 61 | "ids": Array [ 62 | "1", 63 | ], 64 | } 65 | `; 66 | 67 | exports[`Movie Reducer should roll back a deletion if deleting a movie fails 1`] = ` 68 | Object { 69 | "activeMovieId": "1", 70 | "entities": Object { 71 | "1": Object { 72 | "earnings": 10000, 73 | "id": "1", 74 | "name": "Black Panther", 75 | }, 76 | }, 77 | "ids": Array [ 78 | "1", 79 | ], 80 | } 81 | `; 82 | 83 | exports[`Movie Reducer should rollback changes to a movie if there is an error when updating it with the API 1`] = ` 84 | Object { 85 | "activeMovieId": "1", 86 | "entities": Object { 87 | "1": Object { 88 | "earnings": 10000000000, 89 | "id": "1", 90 | "name": "Star Wars: A New Hope", 91 | }, 92 | }, 93 | "ids": Array [ 94 | "1", 95 | ], 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /src/app/shared/state/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import { UserModel } from "../models"; 2 | import { createReducer, on } from "@ngrx/store"; 3 | import { AuthApiActions, AuthUserActions } from "src/app/auth/actions"; 4 | -------------------------------------------------------------------------------- /src/app/shared/state/books.reducer.ts: -------------------------------------------------------------------------------- 1 | import { createReducer, on, Action, createSelector } from "@ngrx/store"; 2 | import { BookModel, calculateBooksGrossEarnings } from "src/app/shared/models"; 3 | import { BooksPageActions, BooksApiActions } from "src/app/books/actions"; 4 | -------------------------------------------------------------------------------- /src/app/shared/state/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducerMap, createSelector, MetaReducer } from "@ngrx/store"; 2 | import * as fromAuth from "./auth.reducer"; 3 | import * as fromBooks from "./books.reducer"; 4 | 5 | export interface State {} 6 | 7 | export const reducers: ActionReducerMap = {}; 8 | 9 | export const metaReducers: MetaReducer[] = []; 10 | -------------------------------------------------------------------------------- /src/app/shared/state/logout.metareducer.ts: -------------------------------------------------------------------------------- 1 | import { ActionReducer, Action } from "@ngrx/store"; 2 | import { AuthUserActions } from "src/app/auth/actions"; 3 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSequence/ngrx-workshop-ngconf2020/9489a335b2f1a5f9f63f248ebe2818649ae30368/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeSequence/ngrx-workshop-ngconf2020/9489a335b2f1a5f9f63f248ebe2818649ae30368/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgRx Workshop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_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 | -------------------------------------------------------------------------------- /src/setupJest.ts: -------------------------------------------------------------------------------- 1 | import "jest-preset-angular"; 2 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "https://fonts.googleapis.com/icon?family=Material+Icons"; 3 | @import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; 4 | 5 | html { 6 | height: 100%; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | font-family: Roboto, sans-serif; 12 | height: 100%; 13 | display: flex; 14 | } 15 | 16 | mat-toolbar-row { 17 | justify-content: space-between; 18 | } 19 | 20 | p { 21 | margin: 16px; 22 | } 23 | 24 | [mat-raised-button] { 25 | width: 100%; 26 | } 27 | 28 | mat-grid-list { 29 | max-width: 1403px; 30 | margin: 16px; 31 | } 32 | 33 | mat-sidenav-layout { 34 | height: 100vh; 35 | } 36 | 37 | mat-sidenav { 38 | width: 320px; 39 | } 40 | 41 | mat-sidenav a { 42 | box-sizing: border-box; 43 | display: block; 44 | font-size: 14px; 45 | font-weight: 400; 46 | line-height: 47px; 47 | text-decoration: none; 48 | -webkit-transition: all 0.3s; 49 | transition: all 0.3s; 50 | padding: 0 16px; 51 | position: relative; 52 | } 53 | 54 | .icon-20 { 55 | font-size: 20px; 56 | } 57 | 58 | * { 59 | -webkit-font-smoothing: antialiased; 60 | -moz-osx-font-smoothing: grayscale; 61 | } 62 | 63 | table { 64 | border-collapse: collapse; 65 | border-radius: 2px; 66 | border-spacing: 0; 67 | margin: 0 0 32px; 68 | width: 100%; 69 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24), 0 0 2px rgba(0, 0, 0, 0.12); 70 | } 71 | 72 | th { 73 | font-size: 16px; 74 | font-weight: 400; 75 | padding: 13px 32px; 76 | text-align: left; 77 | color: rgba(0, 0, 0, 0.54); 78 | background: rgba(0, 0, 0, 0.03); 79 | } 80 | 81 | td { 82 | color: rgba(0, 0, 0, 0.54); 83 | border: 1px solid rgba(0, 0, 0, 0.03); 84 | font-weight: 400; 85 | padding: 8px 30px; 86 | } 87 | 88 | .container { 89 | display: flex; 90 | margin: 10px; 91 | flex-wrap: wrap; 92 | } 93 | 94 | .container [class*="col"] { 95 | padding: 10px; 96 | flex: 1; 97 | } 98 | 99 | mat-card-header .mat-card-header-text { 100 | margin-left: 0; 101 | border-bottom: 1px solid #ffd740; 102 | } 103 | 104 | mat-card-title h1 { 105 | display: inline; 106 | } 107 | 108 | mat-card { 109 | margin-bottom: 20px !important; 110 | } 111 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": ["jest", "node"], 6 | "allowJs": true 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "target": "es2015", 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "typeRoots": ["node_modules/@types"], 18 | "lib": ["es2018", "dom"] 19 | } 20 | } 21 | --------------------------------------------------------------------------------