├── .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 |