├── .gitbook └── assets │ ├── image.png │ ├── image (1).png │ ├── image (10).png │ ├── image (11).png │ ├── image (12).png │ ├── image (13).png │ ├── image (14).png │ ├── image (15).png │ ├── image (16).png │ ├── image (17).png │ ├── image (18).png │ ├── image (2).png │ ├── image (3).png │ ├── image (4).png │ ├── image (5).png │ ├── image (6).png │ ├── image (7).png │ ├── image (8).png │ └── image (9).png ├── SUMMARY.md ├── 16.-use-action-creators-in-reducer-test.md ├── 1.-create-an-application.md ├── 8.-add-test-for-event-emitter.md ├── 10.-test-eventlistcomponent.md ├── 9.-add-eventlistcomponent.md ├── 25.-fix-event-component-tests.md ├── README.md ├── 17.-store-dev-tools.md ├── 7.-listen-to-child-component-events.md ├── 18.-create-selectors.md ├── 14.-test-reducer.md ├── setup.md ├── 3.-test-home-component.md ├── 21.-test-effect.md ├── 22.-use-entity-adapter.md ├── 4.-create-event-feature-module.md ├── 6.-test-addattendeecomponent.md ├── 2.-create-a-home-component.md ├── 18.-create-feature-state.md ├── add-simple-ngrx-spinner.md ├── 12.-test-eventservice.md ├── 23.-add-guests-logic.md ├── 5.create-addattendeecomponent.md ├── 3.-test-homecomponent.md ├── 15.-strongly-type-our-store.md ├── 11.-create-eventservice.md ├── 24.-router-store.md └── 20.-create-effect.md /.gitbook/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image.png -------------------------------------------------------------------------------- /.gitbook/assets/image (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (1).png -------------------------------------------------------------------------------- /.gitbook/assets/image (10).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (10).png -------------------------------------------------------------------------------- /.gitbook/assets/image (11).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (11).png -------------------------------------------------------------------------------- /.gitbook/assets/image (12).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (12).png -------------------------------------------------------------------------------- /.gitbook/assets/image (13).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (13).png -------------------------------------------------------------------------------- /.gitbook/assets/image (14).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (14).png -------------------------------------------------------------------------------- /.gitbook/assets/image (15).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (15).png -------------------------------------------------------------------------------- /.gitbook/assets/image (16).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (16).png -------------------------------------------------------------------------------- /.gitbook/assets/image (17).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (17).png -------------------------------------------------------------------------------- /.gitbook/assets/image (18).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (18).png -------------------------------------------------------------------------------- /.gitbook/assets/image (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (2).png -------------------------------------------------------------------------------- /.gitbook/assets/image (3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (3).png -------------------------------------------------------------------------------- /.gitbook/assets/image (4).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (4).png -------------------------------------------------------------------------------- /.gitbook/assets/image (5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (5).png -------------------------------------------------------------------------------- /.gitbook/assets/image (6).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (6).png -------------------------------------------------------------------------------- /.gitbook/assets/image (7).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (7).png -------------------------------------------------------------------------------- /.gitbook/assets/image (8).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (8).png -------------------------------------------------------------------------------- /.gitbook/assets/image (9).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duncanhunter/angular-and-ngrx-gitbook/HEAD/.gitbook/assets/image (9).png -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | * [Setup](setup.md) 5 | * [1. Create an application](1.-create-an-application.md) 6 | * [2. Create a home component](2.-create-a-home-component.md) 7 | * [3. Test HomeComponent](3.-test-homecomponent.md) 8 | * [4. Create event feature module](4.-create-event-feature-module.md) 9 | * [5.Create AddAttendeeComponent](5.create-addattendeecomponent.md) 10 | * [6. Test AddAttendeeComponent](6.-test-addattendeecomponent.md) 11 | * [7. Listen to child component events](7.-listen-to-child-component-events.md) 12 | * [8. Add test for the event emitter](8.-add-test-for-event-emitter.md) 13 | * [9. Create EventListComponent](9.-add-eventlistcomponent.md) 14 | * [10. Test EventListComponent](10.-test-eventlistcomponent.md) 15 | * [11. Create EventService](11.-create-eventservice.md) 16 | * [12. Test EventService](12.-test-eventservice.md) 17 | * [13. Add simple NgRx spinner](add-simple-ngrx-spinner.md) 18 | * [14. Test reducer](14.-test-reducer.md) 19 | * [15. Strongly type our store](15.-strongly-type-our-store.md) 20 | * [16. Update reducer tests](16.-use-action-creators-in-reducer-test.md) 21 | * [17. Store dev tools](17.-store-dev-tools.md) 22 | * [18. Create selectors](18.-create-selectors.md) 23 | * [19. Create feature state](18.-create-feature-state.md) 24 | * [20. Create effect](20.-create-effect.md) 25 | * [21. Test an effect](21.-test-effect.md) 26 | * [22. Use Entity Adapter](22.-use-entity-adapter.md) 27 | * [23. Add attendee logic](23.-add-guests-logic.md) 28 | * [24. Router store](24.-router-store.md) 29 | * [25. Fix EventComponent tests](25.-fix-event-component-tests.md) 30 | 31 | -------------------------------------------------------------------------------- /16.-use-action-creators-in-reducer-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this little section we will update our reducer tests to use our new types. 3 | --- 4 | 5 | # 16. Update reducer tests 6 | 7 | ## 1. Use action creators in our reducer tests 8 | 9 | * Type our fake effect as an any to trick the TypeScript compiler and also add a StartSpinner action creator to the other test. 10 | 11 | {% code-tabs %} 12 | {% code-tabs-item title="src/app/state/spinner/reducer.spec.ts" %} 13 | ```typescript 14 | import { reducer } from './spinner.reducer'; 15 | import { StartSpinner } from './spinner.actions'; 16 | 17 | describe('Reducer: Spinner', () => { 18 | it('should have initial state of isOn false', () => { 19 | const expected = { isOn: false }; 20 | const action = { type: 'foo' } as any; 21 | expect(reducer(undefined, action)).toEqual(expected); 22 | }); 23 | 24 | it('should have a isOn set to true', () => { 25 | const initialState = { isOn: false }; 26 | const action = new StartSpinner(); 27 | const expected = { isOn: true }; 28 | expect(reducer(initialState, action)).toEqual(expected); 29 | }); 30 | }); 31 | 32 | ``` 33 | {% endcode-tabs-item %} 34 | {% endcode-tabs %} 35 | 36 | ## StackBlitz Link 37 | 38 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/16-update-reducer-tests\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 39 | 40 | -------------------------------------------------------------------------------- /1.-create-an-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will create our application with the Angular CLI and 4 | discuss and examine its parts. 5 | --- 6 | 7 | # 1. Create an application 8 | 9 | * Change directory into the new app's directory and open it with VS Code 10 | 11 | ## 1. Create a new Angular CLI app 12 | 13 | * Run the following command to generate a new Angular application 14 | 15 | ```text 16 | ng new angular-and-ngrx-demo-app --style scss --prefix app 17 | ``` 18 | 19 | Change into the new app's directory and open it with VS Code 20 | 21 | ```text 22 | cd angular-and-ngrx-demo-app 23 | ``` 24 | 25 | ```text 26 | code . 27 | ``` 28 | 29 | ## 2. Run the app {#2-run-the-app} 30 | 31 | * Launch the app with the following command \('s' is short for serve\) 32 | 33 | ```text 34 | ng s 35 | ``` 36 | 37 | Look at output 38 | 39 | ![Bundled javascript files added to index.html dynamically](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LBrKUK581lwLgG0REVS%2F-LETpqwXGsQ61JrR23om%2F-LETqbRMR25KLfmGW0PV%2Fimage.png?alt=media&token=f59945fe-414d-479a-8705-9195806666a9) 40 | 41 | ## 3. Review the structure and key files {#3-review-the-structure-and-key-files} 42 | 43 | * package.json 44 | * index.html 45 | * main.ts 46 | * app.module.ts 47 | * app.component.ts 48 | 49 | ## StackBlitz Link {#3-review-the-structure-and-key-files} 50 | 51 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/1-create-an-application\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Stack blitz web app step 1\"}" %} 52 | 53 | -------------------------------------------------------------------------------- /8.-add-test-for-event-emitter.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will add tests for the AddAttendeeComponent's EventEmitter. 3 | --- 4 | 5 | # 8. Add test for the event emitter 6 | 7 | ## 1. Add test to make sure the component emits an attendee 8 | 9 | Angular has observables as a built in feature. Here we subscribe to the `EventEmitter` which is a special type of Observable called a Subject. Subjects are special as they can both be passed values and listened to with a subscription from an Observer. Here we first set the forms name value and then subscribe and start listening to the `addAttendee` event emitter. We then call the submit method and the subscribe blocks next function is called by the observable. We will dive deeper into observables as the course goes on so if you are new to them do not worry too much for now about the details, just know that subscriptions register an observer with an observable. Feel free to try moving the submit to before the subscription to see it not work as the subscription is registered after the event. 10 | 11 | * Add a test to check the event is emitted on submit. 12 | 13 | {% code-tabs %} 14 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.spec.ts" %} 15 | ```typescript 16 | ... 17 | 18 | it('should emit an attendee', async(() => { 19 | component.addAttendeeForm.controls.name.setValue('Duncan'); 20 | component.addAttendee.subscribe((attendee: Attendee) => { 21 | expect(attendee.name).toEqual('Duncan'); 22 | }); 23 | component.submit(); 24 | })); 25 | 26 | ... 27 | 28 | ``` 29 | {% endcode-tabs-item %} 30 | {% endcode-tabs %} 31 | 32 | ## StackBlitz Link 33 | 34 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/8-add-test-for-event-emitter\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /10.-test-eventlistcomponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will add snapshot tests with Jest that are a very useful 4 | tool whenever you want to make sure your UI does not change unexpectedly. 5 | --- 6 | 7 | # 10. Test EventListComponent 8 | 9 | ## 1. Add a snapshot test 10 | 11 | In this test we will first populate the attendees manually by assigning the variable to a hardcoded value in the test. We then run `fixture.detectChanges` which triggers Angular to run change detection and update the HTML template. Last we do a expectation using the `.toMatchSnapshot` Jest matcher. 12 | 13 | * Add a snapshot test to the EditListComponent. 14 | * The `toMatchSnapshot` matcher might cause an error with the compiler not knowing this symbol, but do not worry it will still work. 15 | 16 | {% code-tabs %} 17 | {% code-tabs-item title="src/app/event/components/event-list/event-list.component.spec.ts" %} 18 | ```typescript 19 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 20 | import { Attendee } from './../../../models'; 21 | 22 | import { EventListComponent } from './event-list.component'; 23 | 24 | describe('EventListComponent', () => { 25 | let component: EventListComponent; 26 | let fixture: ComponentFixture; 27 | 28 | beforeEach(async(() => { 29 | TestBed.configureTestingModule({ 30 | declarations: [ EventListComponent ] 31 | }) 32 | .compileComponents(); 33 | })); 34 | 35 | beforeEach(() => { 36 | fixture = TestBed.createComponent(EventListComponent); 37 | component = fixture.componentInstance; 38 | fixture.detectChanges(); 39 | }); 40 | 41 | it('should create', () => { 42 | expect(component).toBeTruthy(); 43 | }); 44 | 45 | it('should have no attendees on load', () => { 46 | expect(component).toMatchSnapshot(); 47 | }); 48 | 49 | it('should have 1 attendee on load', () => { 50 | component.attendees = [ 51 | {name: 'Duncan', attending: true, guests: 2} 52 | ] as Attendee[]; 53 | 54 | fixture.detectChanges(); 55 | 56 | expect(component).toMatchSnapshot(); 57 | }); 58 | }); 59 | 60 | ``` 61 | {% endcode-tabs-item %} 62 | {% endcode-tabs %} 63 | 64 | ## StackBlitz Link 65 | 66 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/10-test-event-list-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 67 | 68 | -------------------------------------------------------------------------------- /9.-add-eventlistcomponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will make a EventList presentational component that will 4 | show how to use @Input() decorators and pass in data to a child component. 5 | --- 6 | 7 | # 9. Create EventListComponent 8 | 9 | ## 1. Add EventListComponent 10 | 11 | * Add a new presentational component for the event list by running the below command 12 | 13 | ```text 14 | ng g c event/components/event-list 15 | ``` 16 | 17 | ## 2. Add @Input for Attendees to the new component 18 | 19 | * Add @Input for attendees being passed in from the parent component. 20 | 21 | {% code-tabs %} 22 | {% code-tabs-item title="src/app/event/components/event-list/event-list.component.ts" %} 23 | ```typescript 24 | import { Component, Input } from '@angular/core'; 25 | import { Attendee } from '../../../models'; 26 | 27 | @Component({ 28 | selector: 'app-event-list', 29 | templateUrl: './event-list.component.html', 30 | styleUrls: ['./event-list.component.scss'] 31 | }) 32 | export class EventListComponent { 33 | @Input() 34 | attendees: Attendee[]; 35 | } 36 | 37 | 38 | ``` 39 | {% endcode-tabs-item %} 40 | {% endcode-tabs %} 41 | 42 | ## 3. Add property binding to app-event-list component selector 43 | 44 | * Add `app-event-list` selector to `EventComponent` template. 45 | * Add property binding to pass into the child component the attendees. 46 | 47 | {% code-tabs %} 48 | {% code-tabs-item title="src/app/event/containers/event/event.component.html" %} 49 | ```markup 50 | 51 | 52 | 53 | ``` 54 | {% endcode-tabs-item %} 55 | {% endcode-tabs %} 56 | 57 | ## 4. ngFor the attendees on the the page 58 | 59 | * Use an Angular's `*ngFor` to add attendees to the component template by iterating over the `attendees` array. 60 | 61 | {% code-tabs %} 62 | {% code-tabs-item title="src/app/event/components/event-list/event-list.component.html" %} 63 | ```markup 64 | 67 | 68 | ``` 69 | {% endcode-tabs-item %} 70 | {% endcode-tabs %} 71 | 72 | * Run the app and add some attendees and they should be passed down and displayed by the EventListComponent. 73 | 74 | ## StackBlitz Link 75 | 76 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/9-create-event-list-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 77 | 78 | -------------------------------------------------------------------------------- /25.-fix-event-component-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will fix broken tests missing dependencies. 3 | --- 4 | 5 | # 25. Fix EventComponent tests 6 | 7 | ## 1. Fix EventComponent tests 8 | 9 | * Fix the tests by adding the `RouterTestingModule`. 10 | 11 | {% code-tabs %} 12 | {% code-tabs-item title="src/app/event/container/event/event.component.spec.ts" %} 13 | ```typescript 14 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 15 | import { of, BehaviorSubject } from 'rxjs'; 16 | import { Store } from '@ngrx/store'; 17 | import { NO_ERRORS_SCHEMA } from '@angular/compiler/src/core'; 18 | import { HttpClientModule } from '@angular/common/http'; 19 | import { HttpClient } from '@angular/common/http'; 20 | import { RouterTestingModule } from '@angular/router/testing'; 21 | 22 | import { EventComponent } from './event.component'; 23 | import { EventService } from '../../services/event.service'; 24 | import { State } from '../../state'; 25 | import { Attendee } from '../../../models'; 26 | 27 | describe('EventComponent', () => { 28 | let component: EventComponent; 29 | let fixture: ComponentFixture; 30 | let service: EventService; 31 | let store: Store; 32 | 33 | beforeEach(async(() => { 34 | TestBed.configureTestingModule({ 35 | imports: [RouterTestingModule], 36 | providers: [ 37 | { provide: HttpClient, useValue: null }, 38 | { 39 | provide: EventService, 40 | useValue: { 41 | getAttendees: () => {} 42 | } 43 | }, 44 | { 45 | provide: Store, 46 | useValue: { 47 | pipe: () => {}, 48 | dispatch: jest.fn() 49 | } 50 | } 51 | ], 52 | declarations: [EventComponent], 53 | schemas: [NO_ERRORS_SCHEMA] 54 | }).compileComponents(); 55 | })); 56 | 57 | beforeEach(() => { 58 | fixture = TestBed.createComponent(EventComponent); 59 | component = fixture.componentInstance; 60 | service = TestBed.get(EventService); 61 | store = TestBed.get(Store); 62 | fixture.detectChanges(); 63 | }); 64 | 65 | it('should create', () => { 66 | expect(component).toBeTruthy(); 67 | }); 68 | 69 | it('should have a list of attendees set', () => { 70 | 71 | const subject = new BehaviorSubject(null); 72 | 73 | const fakeAttendees = [{ name: 'FAKE_NAME', attending: false, guests: 0 }]; 74 | 75 | jest.spyOn(store, 'pipe').mockImplementation(() => subject); 76 | 77 | subject.next(fakeAttendees); 78 | 79 | component.ngOnInit(); 80 | 81 | component.attendees$.subscribe(attendees => { 82 | expect(attendees).toEqual(fakeAttendees); 83 | }); 84 | }); 85 | }); 86 | 87 | ``` 88 | {% endcode-tabs-item %} 89 | {% endcode-tabs %} 90 | 91 | ## StackBlitz Link 92 | 93 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/25-fix-event-component-tests\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will introduce the course and section slides. 3 | --- 4 | 5 | # Introduction 6 | 7 | ## [Slides for this section](%20https://docs.google.com/presentation/d/1Y7Tf7kjO4Li0ihhkVgRjn4szFJPAkbMvilfrDCbrjq8) 8 | 9 | ## Introduction 10 | 11 | We will look at how to use the Angular CLI to build, scaffold and deploy your angular applications to production. 12 | 13 | We will be 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 Angular and NgRx application you can extend into your applications. 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 excellent Angular applications. 18 | 19 | **Agenda** 20 | 21 | * Angular CLI 22 | * Component architecture with container and presentational components 23 | * Services and HTTP 24 | * Routing 25 | * Reactive forms 26 | * Redux principle 27 | * NgRx store 28 | * Store DevTools 29 | * NgRx effects 30 | * NgRx selectors 31 | * NgRx entity state adapter 32 | * NgRx router actions and effects 33 | * Building and deploying you angular applications 34 | * Unit and e2e testing 35 | 36 | **Prerequisites** 37 | 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](https://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 for the majority of the workshop. 38 | 39 | **Computer Setup** 40 | You need to bring your laptop with the below software installed to follow this workshop: 41 | 42 | * Visual Studio Code 43 | * A command line Git client 44 | * Node.js \(version 8 or later\) 45 | * Angular CLI \(latest version\) 46 | 47 | If you get stuck, we can help you on the day, but it helps to have this already installed. 48 | 49 | ## Repository for the Demo App 50 | 51 | {% embed data="{\"url\":\"https://github.com/duncanhunter/angular-and-ngrx-demo-app\",\"type\":\"link\",\"title\":\"duncanhunter/angular-and-ngrx-demo-app\",\"description\":\"Contribute to duncanhunter/angular-and-ngrx-demo-app development by creating an account on GitHub.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://github.com/fluidicon.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://avatars0.githubusercontent.com/u/2058227?s=400&v=4\",\"width\":400,\"height\":400,\"aspectRatio\":1}}" %} 52 | 53 | ## Repository for the gitbook 54 | 55 | {% embed data="{\"url\":\"https://github.com/duncanhunter/angular-and-ngrx-gitbook\",\"type\":\"link\",\"title\":\"duncanhunter/angular-and-ngrx-gitbook\",\"description\":\"Angular and NgRx Workshop Gitbook Repo. Contribute to duncanhunter/angular-and-ngrx-gitbook development by creating an account on GitHub.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://github.com/fluidicon.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://avatars0.githubusercontent.com/u/2058227?s=400&v=4\",\"width\":400,\"height\":400,\"aspectRatio\":1}}" %} 56 | 57 | ## 58 | 59 | -------------------------------------------------------------------------------- /17.-store-dev-tools.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will introduce the awesome store dev tools for Chrome and 4 | other mediums. 5 | --- 6 | 7 | # 17. Store dev tools 8 | 9 | ## 1. Install redux dev tools chrome extension 10 | 11 | ​[https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en)​ 12 | 13 | ![Image: Redux devtools extension](https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LBrKUK581lwLgG0REVS%2F-LBrWXz_gT2F_dhtsHjY%2F-LBrWZGbEsvKZH3Tzwjq%2Fredux-dev-tools.png?generation=1525644940724530&alt=media) 14 | 15 | ## 2. npm install the ngrx StoreDevtools 16 | 17 | * npm install the store dev tools that expose our reducers to the chrome extension. 18 | 19 | ```text 20 | npm install @ngrx/store-devtools 21 | ``` 22 | 23 | ## 3. Register the StoreDevtoolsModule 24 | 25 | * Register the `StoreDevtoolsModule` in our `AppModule`. 26 | * Name our app in the config to know which app we are dealing with in the browser extension. 27 | * Also disable all the expensive compute and memory heavy functions of the devtools when we are in production to only allow logging. 28 | 29 | {% code-tabs %} 30 | {% code-tabs-item title="src/app/app.module.ts" %} 31 | ```typescript 32 | import { BrowserModule } from '@angular/platform-browser'; 33 | import { NgModule } from '@angular/core'; 34 | import { HttpClientModule } from '@angular/common/http'; 35 | import { RouterModule } from '@angular/router'; 36 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 37 | import { StoreModule } from '@ngrx/store'; 38 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 39 | 40 | import { HomeComponent } from './home/containers/home/home.component'; 41 | import { AppComponent } from './app.component'; 42 | import { InMemoryDataService } from './app.db'; 43 | import { reducer } from './state/spinner/spinner.reducer'; 44 | import { environment } from '../environments/environment.prod'; 45 | 46 | @NgModule({ 47 | declarations: [AppComponent, HomeComponent], 48 | imports: [ 49 | BrowserModule, 50 | RouterModule.forRoot([ 51 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 52 | { path: 'home', component: HomeComponent }, 53 | { path: 'event', loadChildren: './event/event.module#EventModule' } 54 | ]), 55 | HttpClientModule, 56 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { 57 | delay: 1000 58 | }), 59 | StoreModule.forRoot({ spinner: reducer }), 60 | StoreDevtoolsModule.instrument({ 61 | name: 'NgRx Demo App', 62 | logOnly: environment.production 63 | }) 64 | ], 65 | providers: [], 66 | bootstrap: [AppComponent] 67 | }) 68 | export class AppModule {} 69 | 70 | ``` 71 | {% endcode-tabs-item %} 72 | {% endcode-tabs %} 73 | 74 | ## 4. Run the app and explore the dev tools in the browser 75 | 76 | ![App running the store devtools](.gitbook/assets/image%20%2814%29.png) 77 | 78 | ## StackBlitz Link 79 | 80 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/17-store-devtools\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 81 | 82 | -------------------------------------------------------------------------------- /7.-listen-to-child-component-events.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will listen to child component events emitted to the 4 | parent. 5 | --- 6 | 7 | # 7. Listen to child component events 8 | 9 | ## 1. Add event emitter to component 10 | 11 | The `EventEmitter` in Angular is used to emit event from child components to parent components and can pass what ever object, array primitive, null or anything you would like. Event emitters are made by using Angular's`@Output` decorators. We will address the other side of the equation about passing in data to the component with property binding later in the course using the opposite with `@Input` decorators. 12 | 13 | * Add event emitter to component 14 | * Emit add attendee event when the form is submitted by the `submit` method. 15 | 16 | {% code-tabs %} 17 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.ts" %} 18 | ```typescript 19 | import { Component, EventEmitter } from '@angular/core'; 20 | import { Output } from '@angular/core'; 21 | import { Attendee } from '../../../models'; 22 | import { FormGroup } from '@angular/forms'; 23 | import { FormControl } from '@angular/forms'; 24 | import { Validators } from '@angular/forms'; 25 | 26 | @Component({ 27 | selector: 'app-add-attendee', 28 | templateUrl: './add-attendee.component.html', 29 | styleUrls: ['./add-attendee.component.scss'] 30 | }) 31 | export class AddAttendeeComponent { 32 | @Output() 33 | addAttendee = new EventEmitter(); 34 | 35 | addAttendeeForm = new FormGroup({ 36 | name: new FormControl('', [Validators.required]) 37 | }); 38 | 39 | submit() { 40 | const attendee = { 41 | name: this.addAttendeeForm.value.name, 42 | attending: true, 43 | guests: 0 44 | }; 45 | this.addAttendee.emit(attendee); 46 | } 47 | } 48 | ``` 49 | {% endcode-tabs-item %} 50 | {% endcode-tabs %} 51 | 52 | ## 2. Add event listener to add-event-component selector 53 | 54 | * Add the event listener to the add-event-component selector. The $event is an angular specific variable which is a generic name for what ever you emitted from the child component. 55 | 56 | {% code-tabs %} 57 | {% code-tabs-item title="src/app/event/containers/event/event.component.html" %} 58 | ```markup 59 | 60 | ``` 61 | {% endcode-tabs-item %} 62 | {% endcode-tabs %} 63 | 64 | ## 3. Add method to call when event is emitted 65 | 66 | Note we recreate the array every time versus using an array `.push` method which would mutate the array. 67 | 68 | * Add a `addAttendee` method to the component. 69 | * Declare a `attendees` property on the class and initialise it to an empty array. 70 | 71 | {% code-tabs %} 72 | {% code-tabs-item title="/src/app/event/containers/event/event.component.ts" %} 73 | ```typescript 74 | import { Component, OnInit } from '@angular/core'; 75 | import { Attendee } from '../../../models'; 76 | 77 | @Component({ 78 | selector: 'app-event', 79 | templateUrl: './event.component.html', 80 | styleUrls: ['./event.component.scss'] 81 | }) 82 | export class EventComponent implements OnInit { 83 | attendees: Attendee[] = []; 84 | constructor() {} 85 | 86 | ngOnInit() {} 87 | 88 | addAttendee(attendee: Attendee) { 89 | this.attendees = [...this.attendees, attendee]; 90 | console.log('TCL: EventComponent -> addAttendee -> this.attendees', this.attendees); 91 | } 92 | } 93 | ``` 94 | {% endcode-tabs-item %} 95 | {% endcode-tabs %} 96 | 97 | ## StackBlitz Link 98 | 99 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/7-listent-to-child-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 100 | 101 | -------------------------------------------------------------------------------- /18.-create-selectors.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will create selectors in NgRx which are a cornerstone piece 4 | of getting your NgRx architecture right. 5 | --- 6 | 7 | # 18. Create selectors 8 | 9 | ## 1. Add selector file for spinner state 10 | 11 | Selectors are methods used for obtaining slices of store state. @ngrx/store provides a few helper functions for optimising this selection. Selectors are a big deal to help get your architecture right and you can read more in the docs here [https://github.com/ngrx/platform/blob/master/docs/store/selectors.md](https://github.com/ngrx/platform/blob/master/docs/store/selectors.md). 12 | 13 | When using the `createSelector` and `createFeatureSelector`functions @ngrx/store keeps track of the latest arguments in which your selector function was invoked. Because selectors are [pure functions](https://en.wikipedia.org/wiki/Pure_function), the last result can be returned when the arguments match without re-invoking your selector function. This can provide performance benefits, particularly with selectors that perform expensive computation. This practice is known as [memoization](https://en.wikipedia.org/wiki/Memoization). 14 | 15 | * Make a spinner.selectors.ts file. 16 | * Make a `getSpinnerState` using `createFeatureSelector`, we can do this for other different feature state making it possible to join state slices together. 17 | * Make a `getSpinner` selector using the `createSelector` method to return just the boolean value of the `isOn` property. 18 | 19 | {% code-tabs %} 20 | {% code-tabs-item title="src/app/state/spinner/spinner.selectors.ts" %} 21 | ```typescript 22 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 23 | import { State } from './spinner.reducer'; 24 | 25 | export const getSpinnerState = createFeatureSelector('spinner'); 26 | 27 | export const getSpinner = createSelector( 28 | getSpinnerState, 29 | (state: State) => state.isOn 30 | ); 31 | 32 | ``` 33 | {% endcode-tabs-item %} 34 | {% endcode-tabs %} 35 | 36 | ## 2. Use the new selector in our EventComponent 37 | 38 | It is possible to see in this step how selectors clean our u=our code and make it easier for other developer son our team to know what they can get from the store. 39 | 40 | * Use the getSpinner selector versus our inline function where we "dot" into the property on the state tree we want. 41 | 42 | {% code-tabs %} 43 | {% code-tabs-item title="src/app/event/container/event/event.component.ts" %} 44 | ```typescript 45 | 46 | ---------- ABBREVIATED CODE SNIPPET ---------- 47 | 48 | import { Component, OnInit } from '@angular/core'; 49 | import { Observable } from 'rxjs'; 50 | import { Store, select } from '@ngrx/store'; 51 | 52 | import { Attendee } from '../../../models'; 53 | import { EventService } from '../../services/event.service'; 54 | import { State } from '../../../state/state'; 55 | import { StartSpinner, StopSpinner } from '../../../state/spinner/spinner.actions'; 56 | import { getSpinner } from '../../../state/spinner/spinner.selectors'; 57 | 58 | @Component({ 59 | selector: 'app-event', 60 | templateUrl: './event.component.html', 61 | styleUrls: ['./event.component.scss'] 62 | }) 63 | export class EventComponent implements OnInit { 64 | spinner$: Observable; 65 | attendees$: Observable; 66 | 67 | constructor( 68 | private store: Store, 69 | private eventService: EventService 70 | ) {} 71 | 72 | ngOnInit() { 73 | this.getAttendees(); 74 | this.spinner$ = this.store.pipe(select(getSpinner)); 75 | } 76 | 77 | ---------- ABBREVIATED CODE SNIPPET ---------- 78 | ``` 79 | {% endcode-tabs-item %} 80 | {% endcode-tabs %} 81 | 82 | ## StackBlitz Link 83 | 84 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/18-create-selectors\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 85 | 86 | -------------------------------------------------------------------------------- /14.-test-reducer.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will start to see the benefit of using redux in making out 4 | testing simpler as we separate out the concerns of updating state with those 5 | of rendering views in a component. 6 | --- 7 | 8 | # 14. Test reducer 9 | 10 | ## 1. Fix broken EventComponent test 11 | 12 | We need to fix our broken tests every time we add a new dependency to a component or thing under test it will need to be provided to the tests environment. 13 | 14 | * Fix the test by providing a fake `Store` . There are more syntactically simpler ways to deal with this type of faking boiler plate but for now I would like to make it verbose and explicit what we are doing in these tests. 15 | 16 | {% code-tabs %} 17 | {% code-tabs-item title="src/app/event/container/event/event.component.spec.ts" %} 18 | ```typescript 19 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 20 | import { of } from 'rxjs'; 21 | import { Store } from '@ngrx/store'; 22 | import { NO_ERRORS_SCHEMA } from '@angular/compiler/src/core'; 23 | import { HttpClient } from '@angular/common/http'; 24 | 25 | import { EventComponent } from './event.component'; 26 | import { EventService } from '../../services/event.service'; 27 | 28 | describe('EventComponent', () => { 29 | let component: EventComponent; 30 | let fixture: ComponentFixture; 31 | let service: EventService; 32 | beforeEach(async(() => { 33 | TestBed.configureTestingModule({ 34 | providers: [ 35 | { provide: HttpClient, useValue: null }, 36 | { 37 | provide: EventService, 38 | useValue: { 39 | getAttendees: () => {} 40 | } 41 | }, 42 | { 43 | provide: Store, 44 | useValue: { 45 | pipe: () => {} 46 | } 47 | } 48 | ], 49 | declarations: [EventComponent], 50 | schemas: [NO_ERRORS_SCHEMA] 51 | }).compileComponents(); 52 | })); 53 | 54 | beforeEach(() => { 55 | fixture = TestBed.createComponent(EventComponent); 56 | component = fixture.componentInstance; 57 | service = TestBed.get(EventService); 58 | fixture.detectChanges(); 59 | }); 60 | 61 | it('should create', () => { 62 | expect(component).toBeTruthy(); 63 | }); 64 | 65 | it('should have a list of attendees set', () => { 66 | const fakeAttendees = [{ name: 'FAKE_NAME', attending: false, guests: 0 }]; 67 | 68 | jest 69 | .spyOn(service, 'getAttendees') 70 | .mockImplementation(() => of(fakeAttendees)); 71 | 72 | component.ngOnInit(); 73 | 74 | component.attendees$.subscribe(attendees => { 75 | expect(attendees).toEqual(fakeAttendees); 76 | }); 77 | }); 78 | }); 79 | 80 | ``` 81 | {% endcode-tabs-item %} 82 | {% endcode-tabs %} 83 | 84 | ## 2. Test a reducer 85 | 86 | Reducers are so simple to test as they are pure functions. We just need to call the reducer function and pass in a fake piece of state and an action and then check the new state slice returns. 87 | 88 | * Create a spinner.reducer.spec.ts file to the state folder. 89 | * Add a test to check we return the `state` passed in if the cases do not match any action types. 90 | 91 | {% code-tabs %} 92 | {% code-tabs-item title="src/app/state/spinner/reducer.spec.ts" %} 93 | ```typescript 94 | import { reducer } from './spinner.reducer'; 95 | 96 | describe('Reducer: Spinner', () => { 97 | it('should have initial state of isOn false', () => { 98 | const expected = { isOn: false }; 99 | const action = { type: 'foo' } as any; 100 | expect(reducer(undefined, action)).toEqual(expected); 101 | }); 102 | }); 103 | ``` 104 | {% endcode-tabs-item %} 105 | {% endcode-tabs %} 106 | 107 | * Add another test to check we return new `state` with the isOn flag set to true when we pass in a `startSpinner` action. 108 | 109 | {% code-tabs %} 110 | {% code-tabs-item title="src/app/state/spinner/reducer.spec.ts" %} 111 | ```typescript 112 | import { reducer } from './spinner.reducer'; 113 | 114 | describe('Reducer: Spinner', () => { 115 | it('should have initial state of isOn false', () => { 116 | const expected = { isOn: false }; 117 | const action = { type: 'foo' } as any; 118 | expect(reducer(undefined, action)).toEqual(expected); 119 | }); 120 | 121 | it('should have a isOn set to true', () => { 122 | const state = { isOn: false }; 123 | const action = { type: 'startSpinner' }; 124 | const expected = { isOn: true }; 125 | expect(reducer(state, action)).toEqual(expected); 126 | }); 127 | }); 128 | ``` 129 | {% endcode-tabs-item %} 130 | {% endcode-tabs %} 131 | 132 | ## StackBlitz Link 133 | 134 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/14-test-reducer\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 135 | 136 | -------------------------------------------------------------------------------- /setup.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will configure our machines to get the most from this 4 | workshop. 5 | --- 6 | 7 | # Setup 8 | 9 | Dependency checklist: 10 | 11 | 1. node \(version 8 later\) 12 | 2. Git 13 | 3. Angular CLI 14 | 4. Visual Studio Code 15 | 5. Visual Studio Code Extensions 16 | 17 | The main dependency for being able to make an Angular application is node version 8+. The latest stable version of node is best to get if you do not have it already installed. 18 | 19 | ## 1. Install node 20 | 21 | You can check your version of node by running the following command in the terminal. 22 | 23 | ```text 24 | node -v 25 | ``` 26 | 27 | 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](https://github.com/duncanhunter/Enterprise-Angular-Applications-With-NgRx-and-Nx-Book/tree/d63a57a9f1ea36a7623cdf0746dd90b1406edaa2/www.nodejs.org). 28 | 29 | ## 2. Install Git 30 | 31 | You can check your version of node by running the following command in the terminal. 32 | 33 | ```text 34 | git --version 35 | ``` 36 | 37 | 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%20) 38 | 39 | 40 | 41 | ## 3. Install Angular CLI 42 | 43 | We need to have the Angular CLI installed globally. Run the following command. 44 | 45 | ```text 46 | npm install -g @angular/cli 47 | ``` 48 | 49 | ## **4. Get Visual Studio Code** 50 | 51 | {% embed data="{\"url\":\"https://code.visualstudio.com/\",\"type\":\"link\",\"title\":\"Visual Studio Code - Code Editing. Redefined\",\"description\":\"Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.  Visual Studio Code is free and available on your favorite platform - Linux, macOS, and Windows.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://code.visualstudio.com/favicon.ico\",\"width\":128,\"height\":128,\"aspectRatio\":1},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"http://code.visualstudio.com/opengraphimg/opengraph-home.png\",\"width\":1223,\"height\":630,\"aspectRatio\":0.5151267375306623}}" %} 52 | 53 | ![](.gitbook/assets/image%20%2813%29.png) 54 | 55 | ## 5. Get **Visual Studio Code** Extensions 56 | 57 | ![The VSCode extension button](.gitbook/assets/image%20%286%29.png) 58 | 59 | * Angular Essentials: Everything you need for angular in an extension pack 60 | * Rainbow Brackets: Handy for many brackets when inlining observables 61 | * TSLint: Great linting in VS Code 62 | * Wallaby.js for unit tests line 63 | 64 | {% embed data="{\"url\":\"https://marketplace.visualstudio.com/items?itemName=johnpapa.angular-essentials\",\"type\":\"link\",\"title\":\"Angular Essentials - Visual Studio Marketplace\",\"description\":\"Extension for Visual Studio Code - Essential extensions for Angular developers\",\"icon\":{\"type\":\"icon\",\"url\":\"https://marketplace.visualstudio.com/favicon.ico\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://johnpapa.gallerycdn.vsassets.io/extensions/johnpapa/angular-essentials/0.3.2/1508504847990/Microsoft.VisualStudio.Services.Icons.Default\",\"width\":128,\"height\":128,\"aspectRatio\":1},\"caption\":\"Angular essentials extension\"}" %} 65 | 66 | {% embed data="{\"url\":\"https://marketplace.visualstudio.com/items?itemName=2gua.rainbow-brackets\",\"type\":\"link\",\"title\":\"Rainbow Brackets - Visual Studio Marketplace\",\"description\":\"Extension for Visual Studio Code - A rainbow brackets extension for VS Code.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://marketplace.visualstudio.com/favicon.ico\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://2gua.gallerycdn.vsassets.io/extensions/2gua/rainbow-brackets/0.0.6/1474455607820/Microsoft.VisualStudio.Services.Icons.Default\",\"width\":128,\"height\":128,\"aspectRatio\":1},\"caption\":\"Rainbow brackets extension\"}" %} 67 | 68 | {% embed data="{\"url\":\"https://marketplace.visualstudio.com/items?itemName=eg2.tslint\",\"type\":\"link\",\"title\":\"TSLint - Visual Studio Marketplace\",\"description\":\"Extension for Visual Studio Code - TSLint for Visual Studio Code\",\"icon\":{\"type\":\"icon\",\"url\":\"https://marketplace.visualstudio.com/favicon.ico\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://eg2.gallerycdn.vsassets.io/extensions/eg2/tslint/1.0.30/1527489705111/Microsoft.VisualStudio.Services.Icons.Default\",\"width\":120,\"height\":120,\"aspectRatio\":1},\"caption\":\"TSLint extention\"}" %} 69 | 70 | ![WallabyJS extension](.gitbook/assets/image%20%2815%29.png) 71 | 72 | ## 6. Optionally turn on **Visual Studio Code auto save** 73 | 74 | ![VSCode auto save feature](.gitbook/assets/image%20%2818%29.png) 75 | 76 | ## 7. Update VS Code user settings to use single quotes and warnings for lint rules 77 | 78 | * Open the command palette by pressing Ctrl + Shift + P and search for 'Preferences: Open User Settings'. 79 | * Click the ellipsis and select 'Open settings.json' as shown in the following image. 80 | 81 | ![Open User Settings \(settings.json\)](.gitbook/assets/image%20%285%29.png) 82 | 83 | * Add to your user settings the below options. 84 | * Note that many people like to hide the Open Editors explorer on the top right as it is just a list of open tabs which you can see on the tabs themselves. 85 | 86 | ```javascript 87 | { 88 | "tslint.alwaysShowRuleFailuresAsWarnings": true, 89 | "explorer.openEditors.visible": 0, 90 | "prettier.singleQuote": true 91 | } 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /3.-test-home-component.md: -------------------------------------------------------------------------------- 1 | # 3. Test HomeComponent 2 | 3 | {% hint style="info" %} 4 | wallaby.js is a paid extension for VSCode and has a free trial but is not needed to complete this workshop - just a nice to have. 5 | {% endhint %} 6 | 7 | ## 1. Start default karma and jasmine test 8 | 9 | * Execute the below command to view the results of the default test suite using Jasmine and Karma. 10 | 11 | ```text 12 | ng test 13 | ``` 14 | 15 | ## 2. Fix default tests 16 | 17 | * There will be some errors on the AppComponent's spec file since we changed the HTML. We are not so interested in these more complete test examples just yet but we might as well get them working. 18 | 19 | {% code-tabs %} 20 | {% code-tabs-item title="src/app/app.component.spec.ts" %} 21 | ```typescript 22 | import { TestBed, async } from '@angular/core/testing'; 23 | import { RouterTestingModule } from '@angular/router/testing'; 24 | import { AppComponent } from './app.component'; 25 | 26 | describe('AppComponent', () => { 27 | beforeEach(async(() => { 28 | TestBed.configureTestingModule({ 29 | imports: [ RouterTestingModule], 30 | declarations: [ 31 | AppComponent 32 | ], 33 | }).compileComponents(); 34 | })); 35 | it('should create the app', async(() => { 36 | const fixture = TestBed.createComponent(AppComponent); 37 | const app = fixture.debugElement.componentInstance; 38 | expect(app).toBeTruthy(); 39 | })); 40 | it(`should have as title 'angular-and-ngrx-demo-app'`, async(() => { 41 | const fixture = TestBed.createComponent(AppComponent); 42 | const app = fixture.debugElement.componentInstance; 43 | expect(app.title).toEqual('angular-and-ngrx-demo-app'); 44 | })); 45 | it('should render title in a h1 tag', async(() => { 46 | const fixture = TestBed.createComponent(AppComponent); 47 | fixture.detectChanges(); 48 | const compiled = fixture.debugElement.nativeElement; 49 | expect(compiled.querySelector('h1').textContent).toContain('The App'); 50 | })); 51 | }); 52 | ``` 53 | {% endcode-tabs-item %} 54 | {% endcode-tabs %} 55 | 56 | ## 3. Swap out Jasmine for Jest 57 | 58 | {% hint style="warning" %} 59 | It is best you commit you code before running this 'ng add' command so you can both see the changes and revert it if needed. 60 | {% endhint %} 61 | 62 | * You can read more about the benefits of Jest of Jasmine here [jest blog](https://blog.angularindepth.com/integrate-jest-into-an-angular-application-and-library-163b01d977ce). They are both great but we will be using Jest. You can read more about the package [jest package on github](https://github.com/davinkevin/jest). 63 | * Run the below 'ng add' command to add jest to your app. 64 | 65 | ```text 66 | ng add @davinkevin/jest 67 | ``` 68 | 69 | ## 4. Add wallaby.js for unit testing 70 | 71 | * Add wallaby by using the 'ng add' command. This will add wallaby to your Angular CLI application. 72 | 73 | ```text 74 | ng add ngcli-wallaby 75 | ``` 76 | 77 | * We will also need to update this to use jest rather than jasmine as per the sites [instructions](https://wallabyjs.com/docs/integration/angular.html). 78 | 79 | {% code-tabs %} 80 | {% code-tabs-item title="wallaby.js" %} 81 | ```javascript 82 | module.exports = function () { 83 | 84 | const jestTransform = file => require('jest-preset-angular/preprocessor').process(file.content, file.path, {globals: {__TRANSFORM_HTML__: true}, rootDir: __dirname}); 85 | 86 | return { 87 | files: [ 88 | 'src/**/*.+(ts|html|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 89 | 'jest.setup.ts', 90 | '!src/**/*.spec.ts', 91 | ], 92 | 93 | tests: ['src/**/*.spec.ts'], 94 | 95 | env: { 96 | type: 'node', 97 | runner: 'node' 98 | }, 99 | 100 | compilers: { 101 | '**/*.html': file => ({code: jestTransform(file), map: {version: 3, sources: [], names: [], mappings: []}, ranges: []}) 102 | }, 103 | 104 | preprocessors: { 105 | 'src/**/*.js': jestTransform, 106 | }, 107 | 108 | testFramework: 'jest' 109 | }; 110 | }; 111 | ``` 112 | {% endcode-tabs-item %} 113 | {% endcode-tabs %} 114 | 115 | ## 5. Start wallaby 116 | 117 | * Start wallaby.js by pressing "ctrl + shft + p" and searching for wallaby start. 118 | * When prompted for choosing a config file choose "wallaby.js" 119 | 120 | ![VSCode command pallet showing wallaby start](.gitbook/assets/image%20%283%29.png) 121 | 122 | ## 6. Add a simple test for the title property 123 | 124 | {% code-tabs %} 125 | {% code-tabs-item title="src/app/home/home.component.spec.ts" %} 126 | ```typescript 127 | import { HomeComponent } from './home.component'; 128 | 129 | describe('component: home', () => { 130 | test('have a title of "The title"', () => { 131 | const component = new HomeComponent(); 132 | expect(component.title).toEqual('The title'); 133 | }); 134 | }); 135 | ``` 136 | {% endcode-tabs-item %} 137 | {% endcode-tabs %} 138 | 139 | ## StackBlitz Link {#3-review-the-structure-and-key-files} 140 | 141 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/3-test-home-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Stackblitz web view part 3\"}" %} 142 | 143 | -------------------------------------------------------------------------------- /21.-test-effect.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will use marble tests to test our effects observable 4 | streams. 5 | --- 6 | 7 | # 21. Test an effect 8 | 9 | ## 1. Fix broken EventComponent test 10 | 11 | * Add the `dispatch` method to our fake Store provided. 12 | 13 | {% code-tabs %} 14 | {% code-tabs-item title="src/app/event/containers/event/event.component.spec.ts" %} 15 | ```typescript 16 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 17 | 18 | import { EventComponent } from './event.component'; 19 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 20 | import { EventService } from '../../services/event.service'; 21 | import { Store } from '@ngrx/store'; 22 | 23 | describe('EventComponent', () => { 24 | let component: EventComponent; 25 | let fixture: ComponentFixture; 26 | 27 | beforeEach(async(() => { 28 | TestBed.configureTestingModule({ 29 | providers: [ 30 | { 31 | provide: EventService, 32 | useValue: { 33 | getAttendees: () => {}, 34 | } 35 | }, 36 | { 37 | provide: Store, 38 | useValue: { 39 | pipe: () => {}, 40 | dispatch: () => {} 41 | } 42 | } 43 | ], 44 | declarations: [EventComponent], 45 | schemas: [NO_ERRORS_SCHEMA] 46 | }).compileComponents(); 47 | })); 48 | 49 | beforeEach(() => { 50 | fixture = TestBed.createComponent(EventComponent); 51 | component = fixture.componentInstance; 52 | fixture.detectChanges(); 53 | }); 54 | 55 | it('should create', () => { 56 | expect(component).toBeTruthy(); 57 | }); 58 | }); 59 | 60 | ``` 61 | {% endcode-tabs-item %} 62 | {% endcode-tabs %} 63 | 64 | ## 2. Install jest-marbles 65 | 66 | {% hint style="warning" %} 67 | Important to use @latest which is for version 6 of RxJS. 68 | {% endhint %} 69 | 70 | * More docs [https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md). 71 | 72 | ```text 73 | npm i jest-marbles@latest -D 74 | ``` 75 | 76 | ## 3. Add effect spec file 77 | 78 | * Add attendees.effects.spec.ts file. 79 | 80 | {% hint style="info" %} 81 | Unknown why toBeObservable Matcher not working with jest? 82 | {% endhint %} 83 | 84 | {% code-tabs %} 85 | {% code-tabs-item title="src/app/event/state/attendees/attendees.effects.spec.ts" %} 86 | ```typescript 87 | import { TestBed } from '@angular/core/testing'; 88 | import { provideMockActions } from '@ngrx/effects/testing'; 89 | import { hot, cold } from 'jest-marbles'; 90 | import { of, Observable } from 'rxjs'; 91 | 92 | import { AttendeesEffects } from './attendees.effects'; 93 | import { 94 | LoadAttendees, 95 | LoadAttendeesSuccess, 96 | AttendeesActions 97 | } from './attendees.actions'; 98 | import { Attendee } from '../../../models'; 99 | import { EventService } from '../../services/event.service'; 100 | 101 | describe(`Effect: Attendess`, () => { 102 | let actions: Observable; 103 | let effects: AttendeesEffects; 104 | let service: EventService; 105 | 106 | beforeEach(() => { 107 | TestBed.configureTestingModule({ 108 | imports: [], 109 | providers: [ 110 | AttendeesEffects, 111 | provideMockActions(() => actions), 112 | { 113 | provide: EventService, 114 | useValue: { 115 | getAttendees: jest.fn() 116 | } 117 | } 118 | ] 119 | }); 120 | 121 | service = TestBed.get(EventService); 122 | effects = TestBed.get(AttendeesEffects); 123 | }); 124 | 125 | it('should return a cold Observable of LoadAttendeesSuccess action', () => { 126 | const fakeAttendees = [ 127 | { name: 'Duncan', attending: false, guests: 0 } as Attendee 128 | ]; 129 | const action = new LoadAttendees(); 130 | const completion = new LoadAttendeesSuccess(fakeAttendees); 131 | 132 | jest 133 | .spyOn(service, 'getAttendees') 134 | .mockImplementation(() => of(fakeAttendees)); 135 | 136 | actions = hot('--a-', { a: action }); 137 | const expected = cold('--(b)', { b: completion }); 138 | expect(effects.getAttendees$).toBeObservable(expected); 139 | }); 140 | }); 141 | 142 | ``` 143 | {% endcode-tabs-item %} 144 | {% endcode-tabs %} 145 | 146 | ## StackBlitz Link 147 | 148 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/21-test-effect\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 149 | 150 | 151 | 152 | ## Extras and Homework {#extras-and-homework} 153 | 154 | {% hint style="danger" %} 155 | These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises. 156 | 157 | WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course. 158 | 159 | You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you. 160 | {% endhint %} 161 | 162 | ### Read more about testing observables {#convert-the-homecomponent-into-a-feature-module} 163 | 164 | {% embed data="{\"url\":\"https://netbasal.com/testing-observables-in-angular-a2dbbfaf5329\",\"type\":\"link\",\"title\":\"Testing Observables in Angular\",\"description\":\"In this article, I’d like to talk about a misconception I’ve read in other articles about writing tests for observables in Angular.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://cdn-images-1.medium.com/fit/c/304/304/1\*8hD4eYuELoWAbQLNnjQ4mA.jpeg\",\"width\":152,\"height\":152,\"aspectRatio\":1},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://cdn-images-1.medium.com/max/2000/1\*9YJBtwP1j6eXT-aFNNmpUQ.jpeg\",\"width\":2000,\"height\":1333,\"aspectRatio\":0.6665}}" %} 165 | 166 | -------------------------------------------------------------------------------- /22.-use-entity-adapter.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section, we will normalise our state into dictionaries to make it 4 | easier to manage, query and project our state. This is one of the most 5 | important steps to make better redux. 6 | --- 7 | 8 | # 22. Use Entity Adapter 9 | 10 | ## 1. npm install @ngrx/entity 11 | 12 | Entity State adapter for managing record collections. @ngrx/entity provides an API to manipulate and query entity collections. 13 | 14 | 1. Reduces boilerplate for creating reducers that manage a collection of models. 15 | 2. Provides performant CRUD operations for managing entity collections. 16 | 3. Extensible type-safe adapters for selecting entity information. 17 | 18 | * npm install the library 19 | 20 | ```text 21 | npm i @ngrx/entity 22 | ``` 23 | 24 | ## 2. Make an entity adapter 25 | 26 | * make an adapter and use it to make the initial state and manage collections. 27 | 28 | {% code-tabs %} 29 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 30 | ```typescript 31 | <---------- ABBREVIATED CODE SNIPPET ----------> 32 | 33 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 34 | 35 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 36 | import { Attendee } from '../../../models'; 37 | 38 | export interface State extends EntityState { 39 | loading: boolean; 40 | error: any; 41 | } 42 | 43 | const adapter: EntityAdapter = createEntityAdapter(); 44 | 45 | export const intitalState: State = adapter.getInitialState({ 46 | loading: false, 47 | error: null 48 | }); 49 | 50 | export function reducer(state = intitalState, action: AttendeesActions): State { 51 | 52 | <---------- ABBREVIATED CODE SNIPPET ----------> 53 | 54 | ``` 55 | {% endcode-tabs-item %} 56 | {% endcode-tabs %} 57 | 58 | ## 3. Update reducer to use entity adapter collection methods 59 | 60 | The entity adapter will allow us to take a collection and manage it with a set of Adapter Collection Methods. The entity adapter also provides methods for operations against an entity. These methods can change one to many records at a time. Each method returns the newly modified state if changes were made and the same state if no changes were made. You can read more here [https://github.com/ngrx/platform/blob/master/docs/entity/adapter.md](https://github.com/ngrx/platform/blob/master/docs/entity/adapter.md). 61 | 62 | | Method | Action | 63 | | :--- | :--- | 64 | | addOne | Add one entity to the collection | 65 | | addMany | Add multiple entities to the collection | 66 | | addAll | Replace current collection with provided collection | 67 | | removeOne | Remove one entity from the collection | 68 | | removeAll | Clear entity collection | 69 | | updateOne | Update one entity in the collectionaddOne | 70 | | updateMany | Update multiple entities in the collection | 71 | | upsertOne | Add or Update one entity in the collection | 72 | | upsertMany | Add or Update multiple entities in the collection | 73 | 74 | * Change out manual collection management for adapter collection methods. 75 | * Make selectors from the adapter with `adapter.getSelectors` method. 76 | 77 | {% code-tabs %} 78 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 79 | ```typescript 80 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 81 | 82 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 83 | import { Attendee } from '../../../models'; 84 | 85 | export interface State extends EntityState { 86 | loading: boolean; 87 | error: any; 88 | } 89 | 90 | const adapter: EntityAdapter = createEntityAdapter(); 91 | 92 | export const intitalState: State = adapter.getInitialState({ 93 | loading: false, 94 | error: null 95 | }); 96 | 97 | export function reducer(state = intitalState, action: AttendeesActions): State { 98 | switch (action.type) { 99 | case AttendeesActionTypes.LoadAttendees: { 100 | return adapter.removeAll({ 101 | ...state, 102 | loading: false, 103 | error: null 104 | }); 105 | } 106 | 107 | case AttendeesActionTypes.LoadAttendeesSuccess: { 108 | return adapter.addAll(action.payload, { 109 | ...state, 110 | loading: false, 111 | error: null 112 | }); 113 | } 114 | 115 | case AttendeesActionTypes.LoadAttendeesFail: { 116 | return adapter.removeAll({ 117 | ...state, 118 | loading: false, 119 | error: action.payload 120 | }); 121 | } 122 | 123 | default: { 124 | return state; 125 | } 126 | } 127 | } 128 | 129 | export const { 130 | selectIds, 131 | selectEntities, 132 | selectAll, 133 | selectTotal 134 | } = adapter.getSelectors(); 135 | 136 | 137 | ``` 138 | {% endcode-tabs-item %} 139 | {% endcode-tabs %} 140 | 141 | ## 3. Update selectors 142 | 143 | * Update attendee selectors to use the new adapter getSelector methods. 144 | 145 | {% code-tabs %} 146 | {% code-tabs-item title="src/app/event/state/attendees/attendees.selectors.ts" %} 147 | ```typescript 148 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 149 | import * as fromAttendee from './../attendees/attendees.reducer'; 150 | import { EventState } from '..'; 151 | 152 | export const getEventState = createFeatureSelector('event'); 153 | export const getAttendeeState = createSelector( 154 | getEventState, 155 | state => state.attendees 156 | ); 157 | 158 | export const getAttendees = createSelector( 159 | getAttendeeState, 160 | fromAttendee.selectAll 161 | ); 162 | 163 | ``` 164 | {% endcode-tabs-item %} 165 | {% endcode-tabs %} 166 | 167 | 168 | 169 | ![Entity adapter making new ids and entities dictionaries.](.gitbook/assets/image%20%2810%29.png) 170 | 171 | ## StackBlitz Link 172 | 173 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/22-add-entity-adapter\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 174 | 175 | -------------------------------------------------------------------------------- /4.-create-event-feature-module.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will make a feature module and discuss lazy loading feature 4 | state. It will also be the beginning of our demo app that we will grow over 5 | the course. 6 | --- 7 | 8 | # 4. Create event feature module 9 | 10 | ## 1. Create a new EventModule feature module 11 | 12 | * Run the following command to create a new feature module for events. 13 | 14 | ```text 15 | ng g module event 16 | ``` 17 | 18 | ## 2. Add event container component 19 | 20 | We will be following the presentational container component pattern to categorise our components into two groups. The container components are the "smart" components that do all the work to manage state, persisting data and navigating. The presentational components become the "dumb" components mainly focused on displaying data. This makes testing, performance tuning and reusability of components much easier. 21 | 22 | * Run the following command to generate a container component for the event feature. 23 | 24 | ```text 25 | ng g c event/containers/event 26 | ``` 27 | 28 | ## 3. Add event feature module routes 29 | 30 | * Add the router module and a route pointing to the new EventComponent. 31 | 32 | {% code-tabs %} 33 | {% code-tabs-item title="src/app/event/event.module.ts" %} 34 | ```typescript 35 | import { NgModule } from '@angular/core'; 36 | import { CommonModule } from '@angular/common'; 37 | import { RouterModule } from '@angular/router'; 38 | import { EventComponent } from './containers/event/event.component'; 39 | 40 | @NgModule({ 41 | imports: [ 42 | CommonModule, 43 | RouterModule.forChild([ 44 | { path: '', component: EventComponent } 45 | ]) 46 | ], 47 | declarations: [EventComponent] 48 | }) 49 | export class EventModule { } 50 | ``` 51 | {% endcode-tabs-item %} 52 | {% endcode-tabs %} 53 | 54 | ## 4. Add a lazy route to the AppModule 55 | 56 | Lazy loading is done by the router. The "magic string" used in the "loadChildren" field is the magic that allows Angular to compile the JavaScript in this module into a separate bundle. This separate bundle can now be "lazily" sent to the browser when we navigate to the path making our app faster on the initial load. 57 | 58 | * Add a new route to the AppModule. 59 | 60 | {% code-tabs %} 61 | {% code-tabs-item title="src/app/app.module.ts" %} 62 | ```typescript 63 | import { BrowserModule } from '@angular/platform-browser'; 64 | import { NgModule } from '@angular/core'; 65 | import { RouterModule } from '@angular/router'; 66 | 67 | import { AppComponent } from './app.component'; 68 | import { HomeComponent } from './home/containers/home/home.component'; 69 | 70 | @NgModule({ 71 | declarations: [ 72 | AppComponent, 73 | HomeComponent 74 | ], 75 | imports: [ 76 | BrowserModule, 77 | RouterModule.forRoot([ 78 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 79 | { path: 'home', component: HomeComponent }, 80 | { path: 'event', loadChildren: './event/event.module#EventModule' } 81 | ]) 82 | ], 83 | providers: [], 84 | bootstrap: [AppComponent] 85 | }) 86 | export class AppModule { } 87 | ``` 88 | {% endcode-tabs-item %} 89 | {% endcode-tabs %} 90 | 91 | ## 5. Add event and home navigation links to the AppComponent 92 | 93 | The router links in the HTML are very handy to reduce the logic required inside our app to navigate and the injected dependencies needed to do so. 94 | 95 | * Add links to the app component. 96 | 97 | {% code-tabs %} 98 | {% code-tabs-item title="src/app/app.component.html" %} 99 | ```markup 100 |

The App

101 | 105 | ​ 106 | 107 | ``` 108 | {% endcode-tabs-item %} 109 | {% endcode-tabs %} 110 | 111 | * Add a ".menu" class with "display: flex" set, this will make the buttons stay inline in our little menu. 112 | * And a ".router-link-active" class which Angular will use to dynamically add this style to the active link in the button we just made. 113 | 114 | {% code-tabs %} 115 | {% code-tabs-item title="src/app/app.component.scss" %} 116 | ```css 117 | .menu { 118 | display: flex 119 | } 120 | 121 | .router-link-active { 122 | color: blue; 123 | } 124 | ``` 125 | {% endcode-tabs-item %} 126 | {% endcode-tabs %} 127 | 128 | ## 6. Run the app and look at the feature module chunk in the console 129 | 130 | This might seems trivial to do but this is a massive improvement to Angular versus AngularJS \(the first version\) which makes our initial load much smaller and a sub 200kb target in production achievable with some effort. 131 | 132 | * Run the app by running the following command and inspect the terminal seeing the extra chunk created for our feature module \("s" is for serve\). 133 | 134 | ```text 135 | ng s 136 | ``` 137 | 138 | ![Image: Terminal showing compiled chunks including the EventModule JavaScript.](.gitbook/assets/image%20%281%29.png) 139 | 140 | ## StackBlitz link 141 | 142 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/4-create-event-feature-module\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 143 | 144 | ## Extras and Homework 145 | 146 | {% hint style="danger" %} 147 | These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises. 148 | 149 | WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course. 150 | 151 | You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you. 152 | {% endhint %} 153 | 154 | ### Convert the HomeComponent into a feature module 155 | 156 | Steps: 157 | 158 | 1. Add a home module. 159 | 2. Remove all references to the `HomeComponent` from the `AppModule`. 160 | 3. Add the home route to lazy loaded route to the `AppModule`. 161 | 4. Register the `HomeComponent` on the `HomeModule`. 162 | 5. Add the `RouterModule.forChild` to the `HomeModule`. 163 | 6. Check that is works. 164 | 165 | -------------------------------------------------------------------------------- /6.-test-addattendeecomponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will test our new feature component and it's reactive form. 3 | --- 4 | 5 | # 6. Test AddAttendeeComponent 6 | 7 | ## 1. Fix broken tests for the EventComponent 8 | 9 | Here we are using the TestBed API from Angular which helps configure and initialise our environment for unit testing and provides methods for creating components and services in unit tests. It is very similar to an `@ngModule` but has some extra logic to be able to run a test with just this logic and not all the app. 10 | 11 | [NO\_ERRORS\_SCHEMA](https://angular.io/api/core/NO_ERRORS_SCHEMA) will stop your tests from failing due to unknown elements on your HTML templates. You will find you add it to many tests as you do not want to test custom elements or other child elements on your components. 12 | 13 | * Add `NO_ERRORS_SCHEMA` to the broken `EventComponent` test. 14 | 15 | {% code-tabs %} 16 | {% code-tabs-item title="src/app/event/containers/event/event.component.spec.ts" %} 17 | ```typescript 18 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 19 | 20 | import { EventComponent } from './event.component'; 21 | import { NO_ERRORS_SCHEMA } from '@angular/compiler/src/core'; 22 | 23 | describe('EventComponent', () => { 24 | let component: EventComponent; 25 | let fixture: ComponentFixture; 26 | 27 | beforeEach(async(() => { 28 | TestBed.configureTestingModule({ 29 | declarations: [EventComponent], 30 | schemas: [NO_ERRORS_SCHEMA] 31 | }).compileComponents(); 32 | })); 33 | 34 | beforeEach(() => { 35 | fixture = TestBed.createComponent(EventComponent); 36 | component = fixture.componentInstance; 37 | fixture.detectChanges(); 38 | }); 39 | 40 | it('should create', () => { 41 | expect(component).toBeTruthy(); 42 | }); 43 | }); 44 | 45 | ``` 46 | {% endcode-tabs-item %} 47 | {% endcode-tabs %} 48 | 49 | ## 2. Add ReactiveFormsModule to AddAttendeeComponent spec file 50 | 51 | We need to make sure all dependencies our tests need are added available including the ReactiveForms module. 52 | 53 | * Add ReactiveFormsModule to your spec file. 54 | 55 | {% code-tabs %} 56 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.spec.ts" %} 57 | ```typescript 58 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 59 | import { ReactiveFormsModule } from '@angular/forms'; 60 | 61 | import { AddAttendeeComponent } from './add-attendee.component'; 62 | 63 | describe('AddAttendeeComponent', () => { 64 | let component: AddAttendeeComponent; 65 | let fixture: ComponentFixture; 66 | 67 | beforeEach(async(() => { 68 | TestBed.configureTestingModule({ 69 | imports: [ ReactiveFormsModule ], 70 | declarations: [ AddAttendeeComponent ] 71 | }) 72 | .compileComponents(); 73 | })); 74 | 75 | beforeEach(() => { 76 | fixture = TestBed.createComponent(AddAttendeeComponent); 77 | component = fixture.componentInstance; 78 | fixture.detectChanges(); 79 | }); 80 | 81 | it('should create', () => { 82 | expect(component).toBeTruthy(); 83 | }); 84 | }); 85 | 86 | ``` 87 | {% endcode-tabs-item %} 88 | {% endcode-tabs %} 89 | 90 | ## 3. Add test to check our form validation is working 91 | 92 | * Add a tests for when form is loaded to check it is invalid by default. 93 | 94 | {% code-tabs %} 95 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.spec.ts" %} 96 | ```typescript 97 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 98 | import { ReactiveFormsModule } from '@angular/forms'; 99 | 100 | import { AddAttendeeComponent } from './add-attendee.component'; 101 | import { Attendee } from '../../../models'; 102 | 103 | describe('AddAttendeeComponent', () => { 104 | let component: AddAttendeeComponent; 105 | let fixture: ComponentFixture; 106 | 107 | beforeEach(async(() => { 108 | TestBed.configureTestingModule({ 109 | imports: [ReactiveFormsModule], 110 | declarations: [AddAttendeeComponent] 111 | }).compileComponents(); 112 | })); 113 | 114 | beforeEach(() => { 115 | fixture = TestBed.createComponent(AddAttendeeComponent); 116 | component = fixture.componentInstance; 117 | fixture.detectChanges(); 118 | }); 119 | 120 | it('should create', () => { 121 | expect(component).toBeTruthy(); 122 | }); 123 | 124 | it('should have an invalid form on load', () => { 125 | expect(component.addAttendeeForm.invalid).toEqual(false); 126 | }); 127 | }); 128 | 129 | ``` 130 | {% endcode-tabs-item %} 131 | {% endcode-tabs %} 132 | 133 | * Add another test to check the form is valid when the name field is set. 134 | 135 | {% code-tabs %} 136 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.spec.ts" %} 137 | ```typescript 138 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 139 | import { ReactiveFormsModule } from '@angular/forms'; 140 | 141 | import { AddAttendeeComponent } from './add-attendee.component'; 142 | import { Attendee } from '../../../models'; 143 | 144 | describe('AddAttendeeComponent', () => { 145 | let component: AddAttendeeComponent; 146 | let fixture: ComponentFixture; 147 | 148 | beforeEach(async(() => { 149 | TestBed.configureTestingModule({ 150 | imports: [ReactiveFormsModule], 151 | declarations: [AddAttendeeComponent] 152 | }).compileComponents(); 153 | })); 154 | 155 | beforeEach(() => { 156 | fixture = TestBed.createComponent(AddAttendeeComponent); 157 | component = fixture.componentInstance; 158 | fixture.detectChanges(); 159 | }); 160 | 161 | it('should create', () => { 162 | expect(component).toBeTruthy(); 163 | }); 164 | 165 | it('should have an invalid form on load', () => { 166 | expect(component.addAttendeeForm.valid).toEqual(false); 167 | }); 168 | 169 | it('should have a valid form', () => { 170 | component.addAttendeeForm.controls.name.setValue('Duncan'); 171 | expect(component.addAttendeeForm.valid).toEqual(true); 172 | }); 173 | }); 174 | 175 | ``` 176 | {% endcode-tabs-item %} 177 | {% endcode-tabs %} 178 | 179 | ## StackBlitz Link 180 | 181 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/6-test-add-attendee-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /2.-create-a-home-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will add a Home page component and configure an associated 4 | route. 5 | --- 6 | 7 | # 2. Create a home component 8 | 9 | * Update app.component.html as follows. 10 | * Add home component selector to the AppComponent and delete all default HTML. 11 | * Add a router-outlet which we will use in a second to output our custom angular components with Angular's router. This will break our app until we configure the RouterModule in the next step. 12 | * Add the RouterModule and a default route pointing to a 'home' path in the URL i.e. [http://localhost:4200/home](http://localhost:4200/home). 13 | * Add a redirect path to redirect to home if no path is provided. 14 | 15 | The double curly braces are template binding used to put dynamic data onto our HTML. The square braces are for property binding and the round braces event binding. We can access the properties and methods of elements custom or not with the hash symbol called template references. 16 | 17 | {% code-tabs %} 18 | {% code-tabs-item title="src/app/home/containers/home.component.ts" %} 19 | ```typescript 20 | import { Component, OnInit } from '@angular/core'; 21 | 22 | @Component({ 23 | selector: 'app-home', 24 | templateUrl: './home.component.html', 25 | styleUrls: ['./home.component.scss'] 26 | }) 27 | export class HomeComponent implements OnInit { 28 | title: string = 'The title'; 29 | constructor() {} 30 | 31 | ngOnInit() {} 32 | 33 | updateTitle(value) { 34 | console.log(`updateTitle: ${value}`); 35 | this.title = value; 36 | } 37 | } 38 | ``` 39 | {% endcode-tabs-item %} 40 | {% endcode-tabs %} 41 | 42 | * Remove the <app-home></app-home> selector from the AppComponent template, from now on we will just use the router to display the HomeComponent when we navigate to [http://localhost:4200/home](http://localhost:4200/home). 43 | 44 | ## 1. Add a Home page component {#4-add-a-home-page-component} 45 | 46 | * Add home page component using the Angular CLI by running the following in the terminal. 47 | 48 | ```text 49 | ng generate component home/containers/home 50 | ``` 51 | 52 | * Add home component selector to the AppComponent and delete all default HTML. 53 | * Add a router-outlet which we will use in a second to output our custom angular components with Angular's router. This will break our app until we configure the RouterModule in the next step. 54 | * Use the new HomeComponent's selector to add it to the AppComponent's template so we can see adding components with the router and / or the selector of a component. 55 | 56 | {% code-tabs %} 57 | {% code-tabs-item title="src/app/app.component.html" %} 58 | ```markup 59 |

The App

60 | ​ 61 | 62 | 63 | ``` 64 | {% endcode-tabs-item %} 65 | {% endcode-tabs %} 66 | 67 | ## 2. Add a HomeComponent route {#5-add-a-route} 68 | 69 | * Add the RouterModule and a default route pointing to a 'home' path in the url i.e. [http://localhost:4200/home](http://localhost:4200/home) 70 | * Add a redirect path to redirect to home if no path is provided. 71 | 72 | {% code-tabs %} 73 | {% code-tabs-item title="src/app/app.module.ts" %} 74 | ```typescript 75 | import { BrowserModule } from '@angular/platform-browser'; 76 | import { NgModule } from '@angular/core'; 77 | import { RouterModule } from '@angular/router'; 78 | 79 | import { AppComponent } from './app.component'; 80 | import { HomeComponent } from './home/containers/home/home.component'; 81 | 82 | @NgModule({ 83 | declarations: [ 84 | AppComponent, 85 | HomeComponent 86 | ], 87 | imports: [ 88 | BrowserModule, 89 | RouterModule.forRoot([ 90 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 91 | { path: 'home', component: HomeComponent }, 92 | ]) 93 | ], 94 | providers: [], 95 | bootstrap: [AppComponent] 96 | }) 97 | export class AppModule { } 98 | ``` 99 | {% endcode-tabs-item %} 100 | {% endcode-tabs %} 101 | 102 | ## 3. Explore template event and data binding {#6-event-and-data-binding} 103 | 104 | The double curly braces are for template binding used to put dynamic data onto our HTML. The square braces are for property binding and the round braces event binding. We can access the properties and methods of elements custom or not with the hash symbol called template references. 105 | 106 | * Add event and data binding to Home component with a new title property. 107 | 108 | {% code-tabs %} 109 | {% code-tabs-item title="src/app/home/containers/home.component.html" %} 110 | ```markup 111 | {{title}} 112 | 113 | 114 | ``` 115 | {% endcode-tabs-item %} 116 | {% endcode-tabs %} 117 | 118 | * Add a function to the Home component to update the title, output the updated title to the console log, to check it is working. 119 | 120 | {% code-tabs %} 121 | {% code-tabs-item title="src/app/home/containers/home.component.ts" %} 122 | ```typescript 123 | import { Component, OnInit } from '@angular/core'; 124 | 125 | @Component({ 126 | selector: 'app-home', 127 | templateUrl: './home.component.html', 128 | styleUrls: ['./home.component.scss'] 129 | }) 130 | export class HomeComponent implements OnInit { 131 | title: string; 132 | constructor() {} 133 | 134 | ngOnInit() {} 135 | 136 | updateTitle(value) { 137 | console.log(`updateTitle: ${value}`); 138 | this.title = value; 139 | } 140 | } 141 | ``` 142 | {% endcode-tabs-item %} 143 | {% endcode-tabs %} 144 | 145 | * Remove the <app-home></app-home> selector from the AppComponent template, from now on we will just use the router to display the HomeComponent when we are navigated to 'localhost://4200/home' 146 | 147 | ```markup 148 |

The App

149 | ​ 150 | 151 | ``` 152 | 153 | ## StackBlitz Link 154 | 155 | * If you want to see a running version of the workshop up to this point click on the StackBlitz link block below. 156 | 157 | {% hint style="info" %} 158 | If you have no heard of StackBlitz it is Visual Studio Code in the browser. What is even cooler if you look at the url it is loading below it is loading our app from the GitHub repo for the demo app at this branch '2-create-a-home-component'. [https://stackblitz.com/**github**/duncanhunter/angular-and-ngrx-demo-app/tree/**2-create-a-home-component**](https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/2-create-a-home-component)\*\*\*\* 159 | {% endhint %} 160 | 161 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/2-create-a-home-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 162 | 163 | -------------------------------------------------------------------------------- /18.-create-feature-state.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will make a new piece of feature state which will build on 4 | our knowledge from implementing the spinner state. 5 | --- 6 | 7 | # 19. Create feature state 8 | 9 | ## 1. Add state folder and attendee actions 10 | 11 | The best place to start when adding a new slice of state and the associated reducer, effects, selectors and tests is with the action creators. Starting with action creators helps be clear about what you want this section to be able to do and like tests becomes self documenting. 12 | 13 | * Create a state folder and a attendees sub-folder. 14 | * Create a attendees.actions.ts file. 15 | * Add the following actions to load attendees. 16 | 17 | {% code-tabs %} 18 | {% code-tabs-item title="src/app/event/state/attendees/attendees.actions.ts" %} 19 | ```typescript 20 | import { Action } from '@ngrx/store'; 21 | import { Attendee } from '../../../models'; 22 | 23 | export enum AttendeesActionTypes { 24 | LoadAttendees = '[Attendees Page] Load Attendees', 25 | LoadAttendeesSuccess = '[Attendees Page] Load Attendees Success', 26 | LoadAttendeesFail = '[Attendees Page] Load Attendees Fail' 27 | } 28 | 29 | export class LoadAttendees implements Action { 30 | readonly type = AttendeesActionTypes.LoadAttendees; 31 | } 32 | 33 | export class LoadAttendeesSuccess implements Action { 34 | readonly type = AttendeesActionTypes.LoadAttendeesSuccess; 35 | constructor(public payload: Attendee[]) {} 36 | } 37 | 38 | export class LoadAttendeesFail implements Action { 39 | readonly type = AttendeesActionTypes.LoadAttendeesFail; 40 | constructor(public payload: any) {} 41 | } 42 | 43 | export type AttendeesActions = 44 | | LoadAttendees 45 | | LoadAttendeesSuccess 46 | | LoadAttendeesFail; 47 | ``` 48 | {% endcode-tabs-item %} 49 | {% endcode-tabs %} 50 | 51 | ## 2. Create a attendees reducer 52 | 53 | * Create a attendees.reducer.ts file in the state/attendees folder. 54 | * Add a interface called State describing this slice of state. 55 | * Add an initial state that implements this new State interface. 56 | * Add a reducer function that uses our `initialState`, `AttendeesActionTypes` and `AttendeesActions` types. We will come back and talk about this when we do effects in the next section. 57 | 58 | {% code-tabs %} 59 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 60 | ```typescript 61 | import { Attendee } from '../../../models'; 62 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 63 | 64 | export interface State { 65 | attendees: Attendee[]; 66 | loading: boolean; 67 | } 68 | 69 | export const intitalState: State = { 70 | attendees: [], 71 | loading: false 72 | }; 73 | 74 | export function reducer(state = intitalState, action: AttendeesActions): State { 75 | switch (action.type) { 76 | case AttendeesActionTypes.LoadAttendees: 77 | { 78 | } 79 | 80 | break; 81 | 82 | default: { 83 | return state; 84 | } 85 | } 86 | } 87 | 88 | ``` 89 | {% endcode-tabs-item %} 90 | {% endcode-tabs %} 91 | 92 | ## 3. Add index.ts file to expose state logic from feature module 93 | 94 | Index.ts files are like a public API for our feature state that re-exports everything we want to expose to our parts of our Angular application. Having an index.ts file helps other developers know what they should be able to access from this module. 95 | 96 | In this index.ts file we will extend the feature State interface with our global State interface allowing us to have a more complete interface. We can not know what other lazy loaded state will have been loaded so this is not on the interface. 97 | 98 | * Create a index.ts file inside the event/state folder. 99 | * Import our global state name spaced to `fromRoot`. 100 | * Import our attendee state name spaced as `fromAttendees`. 101 | * Make a new `EventState` object that extends the root state. 102 | * Make a `ActionReducerMap` that is a Map of all the reducers in this feature of which we have only one. 103 | 104 | {% code-tabs %} 105 | {% code-tabs-item title="src/app/event/state/index.ts" %} 106 | ```typescript 107 | import { ActionReducerMap } from '@ngrx/store'; 108 | 109 | import * as fromRoot from './../../state/state'; 110 | import * as fromAttendees from './attendees/attendees.reducer'; 111 | 112 | export interface EventState { 113 | attendees: fromAttendees.State; 114 | } 115 | 116 | export interface State extends fromRoot.State { 117 | event: EventState; 118 | } 119 | 120 | export const reducers: ActionReducerMap = { 121 | attendees: fromAttendees.reducer 122 | }; 123 | 124 | ``` 125 | {% endcode-tabs-item %} 126 | {% endcode-tabs %} 127 | 128 | ## 4. Register feature state in the EventModule 129 | 130 | * Register `StoreModule.forFeature` in the `EventModule` and pass in our reducer map from the index.ts file. 131 | * We need to name each piece of state even if it is a map of one or more reducers. This means our state tree will start with properties named after each feature state slice and one for each root state slice like below. 132 | 133 | ```javascript 134 | { 135 | spinner: {...} 136 | event: {...} 137 | someOtherFeatureState: {...} 138 | } 139 | ``` 140 | 141 | {% code-tabs %} 142 | {% code-tabs-item title="src/app/event/event.module.ts" %} 143 | ```typescript 144 | import { NgModule } from '@angular/core'; 145 | import { CommonModule } from '@angular/common'; 146 | import { RouterModule } from '@angular/router'; 147 | import { ReactiveFormsModule } from '@angular/forms'; 148 | import { HttpClientModule } from '@angular/common/http'; 149 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 150 | 151 | import { EventComponent } from './containers/event/event.component'; 152 | import { AddAttendeeComponent } from './components/add-attendee/add-attendee.component'; 153 | import { EventListComponent } from './components/event-list/event-list.component'; 154 | import { StoreModule } from '@ngrx/store'; 155 | import { reducers } from './state'; 156 | @NgModule({ 157 | imports: [ 158 | CommonModule, 159 | RouterModule.forChild([{ path: '', component: EventComponent }]), 160 | ReactiveFormsModule, 161 | HttpClientModule, 162 | StoreModule.forFeature('event', reducers) 163 | ], 164 | declarations: [EventComponent, AddAttendeeComponent, EventListComponent] 165 | }) 166 | export class EventModule {} 167 | ``` 168 | {% endcode-tabs-item %} 169 | {% endcode-tabs %} 170 | 171 | ## 5. Examine the state tree in the devtools 172 | 173 | * Open browser and go to event page 174 | 175 | ![Event page of running app showing state tree with feature state](.gitbook/assets/image.png) 176 | 177 | ## StackBlitz Link 178 | 179 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/19-create-feature-state\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 180 | 181 | -------------------------------------------------------------------------------- /add-simple-ngrx-spinner.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will introduce redux and start converting our demo 4 | application to an NgRx powered application. We will skip strong typing and 5 | action creators till the next section. 6 | --- 7 | 8 | # 13. Add simple NgRx spinner 9 | 10 | ## [Link to section slides](https://docs.google.com/presentation/d/1Y7Tf7kjO4Li0ihhkVgRjn4szFJPAkbMvilfrDCbrjq8/edit#slide=id.g2fa7fd70ec_0_1818) 11 | 12 | ## 1. npm i NgRx Store library 13 | 14 | * npm install @ngrx/store 15 | 16 | ```text 17 | npm i @ngrx/store 18 | ``` 19 | 20 | ## 2. Add a reducer 21 | 22 | Reducers are at the core of the redux pattern NgRx follows. We will have a reducer for each "slice" of state that we will combine into a single "store" that is literally just a JavaScript object. 23 | 24 | This is a very simple reducer, but it follows the basic principles of a reducer. Reducers are just pure functions that take in state and a action \(the instructions to change state\) and return the new state for this slice of state. 25 | 26 | * Create a state folder with a spinner folder inside of it. 27 | * Create a spinner.reducer.ts file and add the below spinner state logic. 28 | 29 | {% code-tabs %} 30 | {% code-tabs-item title="src/app/state/spinner/spinner.reducer.ts" %} 31 | ```typescript 32 | export function reducer(state = { isOn: false }, action) { 33 | switch (action.type) { 34 | case 'startSpinner': { 35 | return { 36 | isOn: true 37 | }; 38 | } 39 | 40 | case 'stopSpinner': { 41 | return { 42 | isOn: false 43 | }; 44 | } 45 | 46 | default: 47 | return state; 48 | } 49 | } 50 | 51 | ``` 52 | {% endcode-tabs-item %} 53 | {% endcode-tabs %} 54 | 55 | ## 3. Register NgRx in app module 56 | 57 | Here we register the reducer we made and name this slice of state "spinner". You will see this piece of state in the dev tools under a property called "spinner", when we add the dev tools in the coming sections. 58 | 59 | * Add NgRx to AppModule 60 | 61 | {% code-tabs %} 62 | {% code-tabs-item title="src/app/app.module.ts" %} 63 | ```typescript 64 | import { BrowserModule } from '@angular/platform-browser'; 65 | import { NgModule } from '@angular/core'; 66 | import { HttpClientModule } from '@angular/common/http'; 67 | import { RouterModule } from '@angular/router'; 68 | 69 | import { AppComponent } from './app.component'; 70 | import { HomeComponent } from './home/containers/home/home.component'; 71 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 72 | import { StoreModule } from '@ngrx/store'; 73 | 74 | import { InMemoryDataService } from './app.db'; 75 | import { reducer } from './state/spinner/spinner.reducer'; 76 | 77 | @NgModule({ 78 | declarations: [AppComponent, HomeComponent], 79 | imports: [ 80 | BrowserModule, 81 | RouterModule.forRoot([ 82 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 83 | { path: 'home', component: HomeComponent }, 84 | { path: 'event', loadChildren: './event/event.module#EventModule' } 85 | ]), 86 | HttpClientModule, 87 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { delay: 1000 }), 88 | StoreModule.forRoot({ spinner: reducer }) 89 | ], 90 | providers: [], 91 | bootstrap: [AppComponent] 92 | }) 93 | export class AppModule {} 94 | 95 | ``` 96 | {% endcode-tabs-item %} 97 | {% endcode-tabs %} 98 | 99 | ## 4. Inject store into the EventComponent 100 | 101 | As we move towards fully implementing NgRx you will see our components become even simpler with just a bunch of subscriptions to the store and actions being dispatched and work normally done in components now delegated to our NgRx system. 102 | 103 | * Inject Store into the EventComponent. 104 | * Select the `spinner$` state. 105 | 106 | {% code-tabs %} 107 | {% code-tabs-item title="src/app/event/container/event/event.component.ts" %} 108 | ```typescript 109 | import { Component, OnInit } from '@angular/core'; 110 | import { Observable } from 'rxjs'; 111 | import { Store, select } from '@ngrx/store'; 112 | 113 | import { Attendee } from '../../../models'; 114 | import { EventService } from '../../services/event.service'; 115 | 116 | @Component({ 117 | selector: 'app-event', 118 | templateUrl: './event.component.html', 119 | styleUrls: ['./event.component.scss'] 120 | }) 121 | export class EventComponent implements OnInit { 122 | spinner$: Observable; 123 | attendees$: Observable; 124 | 125 | constructor(private store: Store, private eventService: EventService) {} 126 | 127 | ngOnInit() { 128 | this.getAttendees(); 129 | this.spinner$ = this.store.pipe(select(state => state.spinner.isOn)); 130 | } 131 | 132 | getAttendees() { 133 | this.attendees$ = this.eventService.getAttendees(); 134 | } 135 | 136 | addAttendee(attendee: Attendee) { 137 | this.eventService.addAttendee(attendee).subscribe(() => { 138 | this.getAttendees(); 139 | }); 140 | } 141 | } 142 | 143 | ``` 144 | {% endcode-tabs-item %} 145 | {% endcode-tabs %} 146 | 147 | ## 5. Dispatch an action when adding new attendees 148 | 149 | * Dispatch a `startSpinner` and `stopSpinner` action when loading and receiving data from the fake backend. 150 | 151 | {% code-tabs %} 152 | {% code-tabs-item title="src/app/event/container/event/event.component.ts" %} 153 | ```typescript 154 | import { Component, OnInit } from '@angular/core'; 155 | import { Observable } from 'rxjs'; 156 | import { Store, select } from '@ngrx/store'; 157 | 158 | import { Attendee } from '../../../models'; 159 | import { EventService } from '../../services/event.service'; 160 | 161 | @Component({ 162 | selector: 'app-event', 163 | templateUrl: './event.component.html', 164 | styleUrls: ['./event.component.scss'] 165 | }) 166 | export class EventComponent implements OnInit { 167 | spinner$: Observable; 168 | attendees$: Observable; 169 | 170 | constructor(private store: Store, private eventService: EventService) {} 171 | 172 | ngOnInit() { 173 | this.getAttendees(); 174 | this.spinner$ = this.store.pipe(select(state => state.spinner.isOn)); 175 | } 176 | 177 | getAttendees() { 178 | this.attendees$ = this.eventService.getAttendees(); 179 | } 180 | 181 | addAttendee(attendee: Attendee) { 182 | this.store.dispatch({ type: 'startSpinner' }); 183 | this.eventService.addAttendee(attendee).subscribe(() => { 184 | this.store.dispatch({ type: 'stopSpinner' }); 185 | this.getAttendees(); 186 | }); 187 | } 188 | } 189 | 190 | ``` 191 | {% endcode-tabs-item %} 192 | {% endcode-tabs %} 193 | 194 | ## 6. Update the EventComponent to show a basic loading indicator 195 | 196 | * Add a loading div with an ngIf. 197 | * Add a `*ngIf` to the `EventListComponent`. 198 | 199 | {% code-tabs %} 200 | {% code-tabs-item title="src/app/event/containers/event.component.html" %} 201 | ```markup 202 | 203 | 204 |
loading..
205 | 206 | ``` 207 | {% endcode-tabs-item %} 208 | {% endcode-tabs %} 209 | 210 | ![Image: Showing 'spinner' state registered in AppModule and used in the component.](.gitbook/assets/image%20%289%29.png) 211 | 212 | ## StackBlitz Link 213 | 214 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/13-simple-ngrx-spinner\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 215 | 216 | -------------------------------------------------------------------------------- /12.-test-eventservice.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will fix the now broken spec files missing newly injected 4 | dependencies in the components and services under test. 5 | --- 6 | 7 | # 12. Test EventService 8 | 9 | ## 1. Fix broken tests with injected dependencies 10 | 11 | * Add the fake EventService and HttpClient to the EventComponent. 12 | 13 | {% code-tabs %} 14 | {% code-tabs-item title="src/app/event/event.component.spec.ts" %} 15 | ```typescript 16 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 17 | import { EventComponent } from './event.component'; 18 | import { NO_ERRORS_SCHEMA } from '@angular/compiler/src/core'; 19 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 20 | 21 | import { EventService } from '../../services/event.service'; 22 | 23 | describe('EventComponent', () => { 24 | let component: EventComponent; 25 | let fixture: ComponentFixture; 26 | 27 | beforeEach(async(() => { 28 | TestBed.configureTestingModule({ 29 | providers: [ 30 | { provide: HttpClient, useValue: null }, 31 | { 32 | provide: EventService, 33 | useValue: { 34 | getAttendees: () => {} 35 | } 36 | } 37 | ], 38 | declarations: [EventComponent], 39 | schemas: [NO_ERRORS_SCHEMA] 40 | }).compileComponents(); 41 | })); 42 | 43 | beforeEach(() => { 44 | fixture = TestBed.createComponent(EventComponent); 45 | component = fixture.componentInstance; 46 | fixture.detectChanges(); 47 | }); 48 | 49 | it('should create', () => { 50 | expect(component).toBeTruthy(); 51 | }); 52 | }); 53 | ``` 54 | {% endcode-tabs-item %} 55 | {% endcode-tabs %} 56 | 57 | ## 2. Fix EventService tests 58 | 59 | * Fix missing injected HttpClient in EventService by using Angular's `HttpClientTestingModule`. 60 | 61 | {% code-tabs %} 62 | {% code-tabs-item title="src/app/event/event.service.spec.ts" %} 63 | ```typescript 64 | import { TestBed, inject } from '@angular/core/testing'; 65 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 66 | 67 | import { EventService } from './event.service'; 68 | 69 | describe('EventService', () => { 70 | beforeEach(() => { 71 | TestBed.configureTestingModule({ 72 | imports: [HttpClientTestingModule], 73 | providers: [EventService] 74 | }); 75 | }); 76 | 77 | it('should be created', inject([EventService], (service: EventService) => { 78 | expect(service).toBeTruthy(); 79 | })); 80 | }); 81 | ``` 82 | {% endcode-tabs-item %} 83 | {% endcode-tabs %} 84 | 85 | ## 3. Add a spy to pass in fake attendees to mock the service 86 | 87 | * Add fake service and spy on it. 88 | 89 | {% code-tabs %} 90 | {% code-tabs-item title="src/app/event/container/event/event.component.spec.ts" %} 91 | ```typescript 92 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 93 | import { EventComponent } from './event.component'; 94 | import { NO_ERRORS_SCHEMA } from '@angular/compiler/src/core'; 95 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 96 | import { of } from 'rxjs'; 97 | 98 | import { EventService } from '../../services/event.service'; 99 | 100 | describe('EventComponent', () => { 101 | let component: EventComponent; 102 | let fixture: ComponentFixture; 103 | let service: EventService; 104 | 105 | beforeEach(async(() => { 106 | TestBed.configureTestingModule({ 107 | providers: [ 108 | { provide: HttpClient, useValue: null }, 109 | { 110 | provide: EventService, 111 | useValue: { 112 | getAttendees: () => {} 113 | } 114 | } 115 | ], 116 | declarations: [EventComponent], 117 | schemas: [NO_ERRORS_SCHEMA] 118 | }).compileComponents(); 119 | })); 120 | 121 | beforeEach(() => { 122 | fixture = TestBed.createComponent(EventComponent); 123 | component = fixture.componentInstance; 124 | service = TestBed.get(EventService); 125 | fixture.detectChanges(); 126 | }); 127 | 128 | it('should create', () => { 129 | expect(component).toBeTruthy(); 130 | }); 131 | 132 | it('should have a list of attendees set', () => { 133 | const fakeAttendees = [{ name: 'FAKE_NAME', attending: false, guests: 0 }]; 134 | 135 | jest 136 | .spyOn(service, 'getAttendees') 137 | .mockImplementation(() => of(fakeAttendees)); 138 | 139 | component.ngOnInit(); 140 | 141 | component.attendees$.subscribe(attendees => { 142 | expect(attendees).toEqual(fakeAttendees); 143 | }); 144 | }); 145 | }); 146 | ``` 147 | {% endcode-tabs-item %} 148 | {% endcode-tabs %} 149 | 150 | ## StackBlitz Link 151 | 152 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/12-test-event-service\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 153 | 154 | 155 | 156 | ## Extras and Homework {#extras-and-homework} 157 | 158 | {% hint style="danger" %} 159 | These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises. 160 | 161 | WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course. 162 | 163 | You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you. 164 | {% endhint %} 165 | 166 | ### Add EventService tests {#convert-the-homecomponent-into-a-feature-module} 167 | 168 | We have no test coverage on our EventService yet. You can read more about how to use Angular's approach to testing HTTP here [https://angular.io/guide/http\#testing-http-requests](https://angular.io/guide/http#testing-http-requests). 169 | 170 | Steps: 171 | 172 | 1. Provide and inject EventService into test. 173 | 2. Make fake attendees array. 174 | 3. Call services getAttendees method and do not forget to subscribe! 175 | 4. Check the path was called. 176 | 5. Verify there are no outstanding requests. 177 | 178 | {% code-tabs %} 179 | {% code-tabs-item title="src/app/event/services/event/event.service.spec.ts" %} 180 | ```typescript 181 | import { TestBed, inject } from '@angular/core/testing'; 182 | import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; 183 | 184 | import { Attendee } from '../../../models'; 185 | import { EventService } from './event.service'; 186 | 187 | describe('EventService', () => { 188 | let httpTestingController: HttpTestingController; 189 | let eventService: EventService; 190 | 191 | beforeEach(() => { 192 | TestBed.configureTestingModule({ 193 | imports: [HttpClientTestingModule], 194 | providers: [EventService] 195 | }); 196 | 197 | eventService = TestBed.get(EventService); 198 | httpTestingController = TestBed.get(HttpTestingController); 199 | }); 200 | 201 | it('should be created', inject([EventService], (service: EventService) => { 202 | expect(service).toBeTruthy(); 203 | })); 204 | 205 | it('can test HttpClient.get attendees', () => { 206 | const testAttendees: Attendee[] = [ 207 | { 208 | name: 'Test Data', 209 | attending: true, 210 | guests: 1 211 | } 212 | ]; 213 | 214 | eventService.getAttendees().subscribe(); 215 | 216 | const req = httpTestingController.expectOne('/api/attendees'); 217 | 218 | expect(req.request.method).toEqual('GET'); 219 | 220 | req.flush(testAttendees); 221 | 222 | httpTestingController.verify(); 223 | }); 224 | }); 225 | 226 | ``` 227 | {% endcode-tabs-item %} 228 | {% endcode-tabs %} 229 | 230 | -------------------------------------------------------------------------------- /23.-add-guests-logic.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will add the logic to add an attendee using NgRx which we 4 | will build on in the rest of the course. 5 | --- 6 | 7 | # 23. Add attendee logic 8 | 9 | ## 1. Add new action creators 10 | 11 | * Add the add ability to our feature state action creators. 12 | 13 | {% code-tabs %} 14 | {% code-tabs-item title="src/app/event/state/attendees/attendees.actions.ts" %} 15 | ```typescript 16 | import { Action } from '@ngrx/store'; 17 | import { Attendee } from '../../../models'; 18 | export enum AttendeesActionTypes { 19 | LoadAttendees = '[Attendees Page] Load Attendees', 20 | LoadAttendeesSuccess = '[Attendees Page] Load Attendees Success', 21 | LoadAttendeesFail = '[Attendees Page] Load Attendees Fail', 22 | AddAttendee = '[Attendee Page] Add Attendee', 23 | AddAttendeeSuccess = '[Attendee API] Add Attendee Success', 24 | AddAttendeeFail = '[Attendee API] Add Attendee Fail' 25 | } 26 | export class LoadAttendees implements Action { 27 | readonly type = AttendeesActionTypes.LoadAttendees; 28 | } 29 | export class LoadAttendeesSuccess implements Action { 30 | readonly type = AttendeesActionTypes.LoadAttendeesSuccess; 31 | constructor(public payload: Attendee[]) {} 32 | } 33 | export class LoadAttendeesFail implements Action { 34 | readonly type = AttendeesActionTypes.LoadAttendeesFail; 35 | constructor(public payload: any) {} 36 | } 37 | 38 | export class AddAttendee implements Action { 39 | readonly type = AttendeesActionTypes.AddAttendee; 40 | constructor(public payload: Attendee) {} 41 | } 42 | export class AddAttendeeSuccess implements Action { 43 | readonly type = AttendeesActionTypes.AddAttendeeSuccess; 44 | constructor(public payload: Attendee) {} 45 | } 46 | export class AddAttendeeFail implements Action { 47 | readonly type = AttendeesActionTypes.AddAttendeeFail; 48 | constructor(public payload: any) {} 49 | } 50 | export type AttendeesActions = 51 | | AddAttendee 52 | | AddAttendeeSuccess 53 | | AddAttendeeFail 54 | | LoadAttendees 55 | | LoadAttendeesSuccess 56 | | LoadAttendeesFail; 57 | ``` 58 | {% endcode-tabs-item %} 59 | {% endcode-tabs %} 60 | 61 | ## 2. Add effect 62 | 63 | * add effect 64 | 65 | {% code-tabs %} 66 | {% code-tabs-item title="src/app/event/state/attendees/attendees.effects.ts" %} 67 | ```typescript 68 | import { Injectable } from '@angular/core'; 69 | import { Actions, Effect } from '@ngrx/effects'; 70 | import { ofType } from '@ngrx/effects'; 71 | import { switchMap, map, catchError } from 'rxjs/operators'; 72 | import { of } from 'rxjs'; 73 | 74 | import { EventService } from '../../services/event.service'; 75 | import { 76 | AttendeesActionTypes, 77 | LoadAttendees, 78 | LoadAttendeesSuccess, 79 | LoadAttendeesFail, 80 | AddAttendee, 81 | AddAttendeeSuccess, 82 | AddAttendeeFail 83 | } from './attendees.actions'; 84 | import { Attendee } from '../../../models'; 85 | 86 | @Injectable() 87 | export class AttendeesEffects { 88 | constructor(private actions$: Actions, private eventService: EventService) {} 89 | 90 | @Effect() 91 | getAttendees$ = this.actions$.pipe( 92 | ofType(AttendeesActionTypes.LoadAttendees), 93 | switchMap((action: LoadAttendees) => 94 | this.eventService.getAttendees().pipe( 95 | map((attendees: Attendee[]) => new LoadAttendeesSuccess(attendees)), 96 | catchError(error => of(new LoadAttendeesFail(error))) 97 | ) 98 | ) 99 | ); 100 | 101 | @Effect() 102 | addAttendee$ = this.actions$.pipe( 103 | ofType(AttendeesActionTypes.AddAttendee), 104 | switchMap((action: AddAttendee) => 105 | this.eventService.addAttendee(action.payload).pipe( 106 | map((attendee: Attendee) => new AddAttendeeSuccess(attendee)), 107 | catchError(error => of(new AddAttendeeFail(error))) 108 | ) 109 | ) 110 | ); 111 | } 112 | ``` 113 | {% endcode-tabs-item %} 114 | {% endcode-tabs %} 115 | 116 | ## 3. Update reducer 117 | 118 | * Update reducer to have a case for adding one attendee to the store using the `adapter.addOne` method. 119 | 120 | {% code-tabs %} 121 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 122 | ```typescript 123 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 124 | 125 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 126 | import { Attendee } from '../../../models'; 127 | 128 | export interface State extends EntityState { 129 | loading: boolean; 130 | error: any; 131 | } 132 | 133 | const adapter: EntityAdapter = createEntityAdapter(); 134 | 135 | export const intitalState: State = adapter.getInitialState({ 136 | loading: false, 137 | error: null 138 | }); 139 | 140 | export function reducer(state = intitalState, action: AttendeesActions): State { 141 | switch (action.type) { 142 | case AttendeesActionTypes.LoadAttendees: { 143 | return adapter.removeAll({ 144 | ...state, 145 | loading: false, 146 | error: null 147 | }); 148 | } 149 | 150 | case AttendeesActionTypes.LoadAttendeesSuccess: { 151 | return adapter.addAll(action.payload, { 152 | ...state, 153 | loading: false, 154 | error: null 155 | }); 156 | } 157 | 158 | case AttendeesActionTypes.LoadAttendeesFail: { 159 | return adapter.removeAll({ 160 | ...state, 161 | loading: false, 162 | error: action.payload 163 | }); 164 | } 165 | 166 | case AttendeesActionTypes.AddAttendeeSuccess: { 167 | return adapter.addOne(action.payload, { ...state, error: null }); 168 | } 169 | 170 | case AttendeesActionTypes.AddAttendeeFail: { 171 | return { ...state, error: action.payload }; 172 | } 173 | 174 | default: { 175 | return state; 176 | } 177 | } 178 | } 179 | 180 | export const { 181 | selectIds, 182 | selectEntities, 183 | selectAll, 184 | selectTotal 185 | } = adapter.getSelectors(); 186 | ``` 187 | {% endcode-tabs-item %} 188 | {% endcode-tabs %} 189 | 190 | ## 4. Update EventComponent to dispatch AddAttendees 191 | 192 | * Update component and remove spinner, we can now use loading state property for the attendees slice of state. 193 | 194 | {% code-tabs %} 195 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 196 | ```typescript 197 | import { Component, OnInit } from '@angular/core'; 198 | import { Observable } from 'rxjs'; 199 | import { Store, select } from '@ngrx/store'; 200 | 201 | import { Attendee } from '../../../models'; 202 | import { EventService } from '../../services/event.service'; 203 | import { 204 | StartSpinner, 205 | StopSpinner 206 | } from '../../../state/spinner/spinner.actions'; 207 | import { getSpinner } from '../../../state/spinner/spinner.selectors'; 208 | import { 209 | LoadAttendees, 210 | AddAttendee 211 | } from '../../state/attendees/attendees.actions'; 212 | import { State } from '../../state'; 213 | import { getAttendees } from '../../state/attendees/attendees.selectors'; 214 | 215 | @Component({ 216 | selector: 'app-event', 217 | templateUrl: './event.component.html', 218 | styleUrls: ['./event.component.scss'] 219 | }) 220 | export class EventComponent implements OnInit { 221 | spinner$: Observable; 222 | attendees$: Observable; 223 | 224 | constructor( 225 | private store: Store, 226 | private eventService: EventService 227 | ) {} 228 | 229 | ngOnInit() { 230 | this.attendees$ = this.store.pipe(select(getAttendees)); 231 | this.store.dispatch(new LoadAttendees()); 232 | } 233 | 234 | addAttendee(attendee: Attendee) { 235 | this.store.dispatch(new AddAttendee(attendee)); 236 | } 237 | } 238 | ``` 239 | {% endcode-tabs-item %} 240 | {% endcode-tabs %} 241 | 242 | ## StackBlitz Link 243 | 244 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/23-add-attendee-logic\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 245 | 246 | -------------------------------------------------------------------------------- /5.create-addattendeecomponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will make a presentational component to add attendees. 3 | --- 4 | 5 | # 5.Create AddAttendeeComponent 6 | 7 | ## 1. Create add AddAttendeeComponent 8 | 9 | * Add the component with the following command 10 | 11 | ```text 12 | ng g c event/components/add-attendee 13 | ``` 14 | 15 | ## 2. Create a Attendee interface 16 | 17 | It is encouraged to make many custom interfaces and discouraged to leave code un-typed. It is important to note these interfaces do not get added to the JavaScript bundled and sent to the browser, so adding them to a global folder like this is acceptable in this respect. 18 | 19 | * Create a models folder on the root of the app folder. 20 | * Add a Attendee interface file to the models folder with the below code. 21 | 22 | {% code-tabs %} 23 | {% code-tabs-item title="src/app/models/attendee.ts" %} 24 | ```typescript 25 | export interface Attendee { 26 | name: string; 27 | guests: number; 28 | attending: boolean; 29 | } 30 | 31 | ``` 32 | {% endcode-tabs-item %} 33 | {% endcode-tabs %} 34 | 35 | In Angular and NgRx in particular it is very common to make an index.ts file which acts as a public API for a section of code like a models folder or a feature module. Everything that is meant to be shared from this section of code is described here, helping other developers know what by design is to be consumed from each section of an app. 36 | 37 | * Create an index.ts file to the models folder to re-export all the interfaces we make as we go, starting with our Attendee interface. 38 | 39 | {% code-tabs %} 40 | {% code-tabs-item title="src/app/models/index.ts" %} 41 | ```typescript 42 | export * from './attendee'; 43 | 44 | ``` 45 | {% endcode-tabs-item %} 46 | {% endcode-tabs %} 47 | 48 | ## 3. Add AddAttendeeComponent selector to event container 49 | 50 | * Add the AddAttendeeComponent to the EventComponent. 51 | 52 | {% code-tabs %} 53 | {% code-tabs-item title="src/app/event/containers/event/event.component.html" %} 54 | ```markup 55 | 56 | ``` 57 | {% endcode-tabs-item %} 58 | {% endcode-tabs %} 59 | 60 | ## 4. Add ReactiveFormsModule to EventModule 61 | 62 | In Angular you have "Template Driven" and "Reactive Forms". Reactive forms are the newer of the two and the recommended approach from the Angular team. Reactive forms are powerful and flexible with the ability to configure in code making it easy to change forms dynamically, listening to field changes as observables and make custom asynchronous validation simple. 63 | 64 | * Import the ReactiveFormsModule into EventModule and register it in the imports array of the @NgModule decorator. 65 | 66 | {% code-tabs %} 67 | {% code-tabs-item title="src/app/event/event.module.ts" %} 68 | ```typescript 69 | import { NgModule } from '@angular/core'; 70 | import { CommonModule } from '@angular/common'; 71 | import { RouterModule } from '@angular/router'; 72 | import { ReactiveFormsModule } from '@angular/forms'; 73 | 74 | import { EventComponent } from './containers/event/event.component'; 75 | import { AddAttendeeComponent } from './components/add-attendee/add-attendee.component'; 76 | 77 | @NgModule({ 78 | imports: [ 79 | CommonModule, 80 | RouterModule.forChild([{ path: '', component: EventComponent }]), 81 | ReactiveFormsModule 82 | ], 83 | declarations: [EventComponent, AddAttendeeComponent] 84 | }) 85 | export class EventModule {} 86 | 87 | ``` 88 | {% endcode-tabs-item %} 89 | {% endcode-tabs %} 90 | 91 | ## 5. Add input and reactive form to AddAttendeeComponent 92 | 93 | To keep this presentational component simple and avoid injecting in dependencies making it harder to mock we will not use the popular [FormBuilder](https://angular.io/api/forms/FormBuilder) from Angular. We only have a single field or`FormControl` in our `FormGroup` but it is easy to imagine how we could add more to this `addAttendeeForm`. 94 | 95 | * Remove the `constructor` and `ngOnInit` lifecycle hook from the component, further helping other developers know this is a presentational component and should not need a constructor. 96 | * Add `addAttendeeForm` reactive form, `FormGroup` and `FormControl`. 97 | 98 | {% code-tabs %} 99 | {% code-tabs-item title="src\\app\\event\\components\\add-attendee\\add-attendee.component.ts" %} 100 | ```typescript 101 | import { Component } from '@angular/core'; 102 | import { Attendee } from '../../../models'; 103 | import { FormGroup } from '@angular/forms'; 104 | import { FormControl } from '@angular/forms'; 105 | import { Validators } from '@angular/forms'; 106 | 107 | @Component({ 108 | selector: 'app-add-attendee', 109 | templateUrl: './add-attendee.component.html', 110 | styleUrls: ['./add-attendee.component.scss'] 111 | }) 112 | export class AddAttendeeComponent { 113 | addAttendeeForm = new FormGroup({ 114 | name: new FormControl('', [Validators.required]) 115 | }); 116 | } 117 | 118 | ``` 119 | {% endcode-tabs-item %} 120 | {% endcode-tabs %} 121 | 122 | * Add `submit` method to be executed when someone hits the add button in the HTML template. 123 | * In the `submit` method build up a new `attendee` constant which we can add on the attending and guests fields we have not yet implemented in our form. 124 | * Console log the submitted value until the next section when we emit the submitted form out of this presentational component to be listened to by the parent container component. 125 | 126 | {% hint style="info" %} 127 | I am using the [Turbo Console Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=ChakrounAnas.turbo-console-log) to make more readable log messages. 128 | {% endhint %} 129 | 130 | {% code-tabs %} 131 | {% code-tabs-item title="src\\app\\event\\components\\add-attendee\\add-attendee.component.ts" %} 132 | ```typescript 133 | import { Component } from '@angular/core'; 134 | import { FormGroup, FormControl, Validators } from '@angular/forms'; 135 | 136 | import { Attendee } from '../../../models'; 137 | 138 | @Component({ 139 | selector: 'app-add-attendee', 140 | templateUrl: './add-attendee.component.html', 141 | styleUrls: ['./add-attendee.component.scss'] 142 | }) 143 | export class AddAttendeeComponent { 144 | addAttendeeForm = new FormGroup({ 145 | name: new FormControl('', [Validators.required]) 146 | }); 147 | 148 | submit() { 149 | const attendee = { 150 | name: this.addAttendeeForm.value.name, 151 | attending: true, 152 | guests: 0 153 | }; 154 | console.log('TCL: AddAttendeeComponent -> submit -> attendee', attendee); 155 | } 156 | } 157 | ``` 158 | {% endcode-tabs-item %} 159 | {% endcode-tabs %} 160 | 161 | ## 6. Add form to component HTML 162 | 163 | * Create a HTML form element and add an angular `[formGroup]` binding to angularize the form. 164 | * and `(ngSubmit)` event handler to call our submit method when the form is submitted. We do not need to pass the form value over we can grab in in the template from the `addAttendeeForm`. 165 | * Bind the name field of our FormControl to the input element with angular's `formControlName` attribute. 166 | 167 | {% code-tabs %} 168 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.html" %} 169 | ```markup 170 |
171 | 172 | 173 |
174 | 175 | ``` 176 | {% endcode-tabs-item %} 177 | {% endcode-tabs %} 178 | 179 | ## 7. Add CSS styles to component SCSS file 180 | 181 | * Add styles to make the button and input element inline next to each other. 182 | 183 | {% code-tabs %} 184 | {% code-tabs-item title="src/app/event/components/add-attendee/add-attendee.component.scss" %} 185 | ```css 186 | form { 187 | display: flex; 188 | } 189 | 190 | ``` 191 | {% endcode-tabs-item %} 192 | {% endcode-tabs %} 193 | 194 | ## 8. Submit form and check it's value 195 | 196 | * Start the app and check the correct value is logged to the console when you submit a name. 197 | 198 | ## StackBlitz Link 199 | 200 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/5-create-add-attendee-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /3.-test-homecomponent.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will introduce unit testing. 3 | --- 4 | 5 | # 3. Test HomeComponent 6 | 7 | {% hint style="info" %} 8 | We will be demonstrating wallaby.js in this workshop which is a paid extension for VS Code and free trial but is not needed to complete this workshop - just a nice to have. 9 | {% endhint %} 10 | 11 | * Execute the below command to see results of running the default test suite of Jasmine and Karma. You will have broken tests that were generated when the Angular CLI made default tests. 12 | * There will be some errors in the AppComponent's spec file since we changed the HTML. We are not so interested in these more complete test examples just yet, but we need to get them working before moving on. Copy and paste the code below in app.component.spec.ts, or optionally feel free to delete them as we will build up writing tests like these. 13 | 14 | {% hint style="warning" %} 15 | It is a good idea to commit your code before running the following 'ng add' command so you can see both changes and revert it if needed. 16 | {% endhint %} 17 | 18 | You can read more about the benefits of using Jest over Jasmine for your Angular unit tests at the [jest blog](https://blog.angularindepth.com/integrate-jest-into-an-angular-application-and-library-163b01d977ce). They are both great but we will be using Jest. You can read more about the Angular [jest package on github](https://github.com/davinkevin/jest). 19 | 20 | * Add the necessary package dependencies first. 21 | * Run the 'ng add' command to add jest to your app. The 'ng add' commands allow us to add and also configure our app saving many tedious error prone steps doing it by hand. 22 | 23 | {% hint style="warning" %} 24 | Commit your changes before running any 'ng add' or Angular CLI commands so it is easy to use source control to undo if it is wrong. 25 | {% endhint %} 26 | 27 | * Add wallaby by using this 'ng add' command. This will add wallaby to your Angular CLI application. 28 | 29 | ## [Link to section slides](https://docs.google.com/presentation/d/1Y7Tf7kjO4Li0ihhkVgRjn4szFJPAkbMvilfrDCbrjq8/edit#slide=id.g4271862451_0_24) 30 | 31 | {% hint style="info" %} 32 | We will be demonstrating wallaby.js in this workshop which is a paid extension for VSCode and has a free trial but is not needed to complete this workshop just a nice to have. 33 | {% endhint %} 34 | 35 | ## 1. Start default Karma and Jasmine tests 36 | 37 | * Execute the below command to view the results of the default test suite using Jasmine and Karma. You will have broken tests that where generated when the AngularCLI made default tests. 38 | 39 | ```text 40 | ng test 41 | ``` 42 | 43 | ## 2. Fix default tests 44 | 45 | * There will be some errors on the AppComponent's spec file since we changed the HTML. We are not so interested in these more complete test examples just yet but we might as well get them working. Optionally feel free to delete them as we will build up writing tests like these. 46 | 47 | {% code-tabs %} 48 | {% code-tabs-item title="src/app/app.component.spec.ts" %} 49 | ```typescript 50 | import { TestBed, async } from '@angular/core/testing'; 51 | import { RouterTestingModule } from '@angular/router/testing'; 52 | import { AppComponent } from './app.component'; 53 | 54 | describe('AppComponent', () => { 55 | beforeEach(async(() => { 56 | TestBed.configureTestingModule({ 57 | imports: [ RouterTestingModule], 58 | declarations: [ 59 | AppComponent 60 | ], 61 | }).compileComponents(); 62 | })); 63 | it('should create the app', async(() => { 64 | const fixture = TestBed.createComponent(AppComponent); 65 | const app = fixture.debugElement.componentInstance; 66 | expect(app).toBeTruthy(); 67 | })); 68 | it(`should have as title 'angular-and-ngrx-demo-app'`, async(() => { 69 | const fixture = TestBed.createComponent(AppComponent); 70 | const app = fixture.debugElement.componentInstance; 71 | expect(app.title).toEqual('angular-and-ngrx-demo-app'); 72 | })); 73 | it('should render title in a h1 tag', async(() => { 74 | const fixture = TestBed.createComponent(AppComponent); 75 | fixture.detectChanges(); 76 | const compiled = fixture.debugElement.nativeElement; 77 | expect(compiled.querySelector('h1').textContent).toContain('The App'); 78 | })); 79 | }); 80 | ``` 81 | {% endcode-tabs-item %} 82 | {% endcode-tabs %} 83 | 84 | ## 3. Swap out Jasmine for Jest 85 | 86 | {% hint style="warning" %} 87 | It is best you commit you code before running this 'ng add' command so you can both see the changes and revert it if needed. 88 | {% endhint %} 89 | 90 | You can read more about the benefits of using Jest over Jasmine for your Angular unit tests here [jest blog](https://blog.angularindepth.com/integrate-jest-into-an-angular-application-and-library-163b01d977ce). They are both great but we will be using Jest. You can read more about the package the Angular [jest package on github](https://github.com/davinkevin/jest). 91 | 92 | * Best to add the needed package dependencies first. 93 | 94 | ```text 95 | npm i jest-preset-angular jest -D 96 | ``` 97 | 98 | * Run the below 'ng add' command to add jest to your app. The 'ng add' commands allow us to add and also configure our app saving many tedious error prone steps doing it by hand. 99 | 100 | ```text 101 | ng add @davinkevin/jest 102 | ``` 103 | 104 | * Check it is working by running the following command 105 | 106 | ```text 107 | npm test 108 | ``` 109 | 110 | ![Image: Terminal showing test runner output of passing tests](.gitbook/assets/image%20%282%29.png) 111 | 112 | ## 4. Add wallaby.js for unit testing 113 | 114 | * !Commit your changes before running any 'ng add' or Angular CLI commands so it is easy to use source control to undo if it is wrong. 115 | * Add wallaby by using this 'ng add' command. This will add wallaby to your Angular CLI application. 116 | 117 | ```text 118 | ng add ngcli-wallaby 119 | ``` 120 | 121 | * We will also need to update this to use jest rather than jasmine as per the sites [instructions](https://wallabyjs.com/docs/integration/angular.html). 122 | 123 | {% code-tabs %} 124 | {% code-tabs-item title="wallaby.js" %} 125 | ```javascript 126 | module.exports = function () { 127 | 128 | const jestTransform = file => require('jest-preset-angular/preprocessor').process(file.content, file.path, {globals: {__TRANSFORM_HTML__: true}, rootDir: __dirname}); 129 | 130 | return { 131 | files: [ 132 | 'src/**/*.+(ts|html|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 133 | 'jest.setup.ts', 134 | '!src/**/*.spec.ts', 135 | ], 136 | 137 | tests: ['src/**/*.spec.ts'], 138 | 139 | env: { 140 | type: 'node', 141 | runner: 'node' 142 | }, 143 | 144 | compilers: { 145 | '**/*.html': file => ({code: jestTransform(file), map: {version: 3, sources: [], names: [], mappings: []}, ranges: []}) 146 | }, 147 | 148 | preprocessors: { 149 | 'src/**/*.js': jestTransform, 150 | }, 151 | 152 | testFramework: 'jest' 153 | }; 154 | }; 155 | ``` 156 | {% endcode-tabs-item %} 157 | {% endcode-tabs %} 158 | 159 | ## 5. Start wallaby 160 | 161 | {% hint style="info" %} 162 | You should have already installed the WallabyJS extension for Visual Studio Code, if not then please follow these instructions to do so [https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode](https://marketplace.visualstudio.com/items?itemName=WallabyJs.wallaby-vscode). 163 | {% endhint %} 164 | 165 | * Start wallaby.js by pressing "Ctrl + Shift + P" and searching for wallaby start. 166 | * When prompted for choosing a config file choose "wallaby.js" 167 | 168 | ![Image: Visual Studio Code command pallet showing wallaby start](.gitbook/assets/image%20%288%29.png) 169 | 170 | ## 6. Add a simple test for the title property of the HomeComponent 171 | 172 | Let's write some simple component tests on our HomeComponent which we will grow on over this course to write more advanced tests. In this test we use Jest's Behaviour Driven Design \(BDD\) style tests. Do not be concerned about using Jest rather than Jasmine as the default testing tools for Angular because the syntax is virtually the same except for Spys, which we will point out later the differences. 173 | 174 | The "describe" block is for suites of tests and the "it" block is an individual test. In this test we simply new up the HomeComponent and check its title property, later we will use Angular's TestBed test helper API to set up our tests. 175 | 176 | * Delete the code generated tests in the home.component.spec.ts file we will start from scratch before tackling the auto-generated tests. 177 | * Write a simple test for the HomeComponent. 178 | 179 | {% code-tabs %} 180 | {% code-tabs-item title="src/app/home/home.component.spec.ts" %} 181 | ```typescript 182 | import { HomeComponent } from './home.component'; 183 | 184 | describe('component: home', () => { 185 | it('have a title of "The title"', () => { 186 | const component = new HomeComponent(); 187 | expect(component.title).toEqual('The title'); 188 | }); 189 | }); 190 | ``` 191 | {% endcode-tabs-item %} 192 | {% endcode-tabs %} 193 | 194 | ## StackBlitz Link {#3-review-the-structure-and-key-files} 195 | 196 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/3-test-home-component\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 197 | 198 | -------------------------------------------------------------------------------- /15.-strongly-type-our-store.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will add strong typing using TypeScript to our applications 4 | state logic. We skipped this step to make it simple to see what redux is and 5 | how the pattern works. 6 | --- 7 | 8 | # 15. Strongly type our store 9 | 10 | ## 1. Add types to our reducer 11 | 12 | By community convention most people put the types for each slice of state with the reducer, which makes sense as this is where you update the slice of state. Here will will make an interface for shape of the slice of state and use it to type a constant to make our initial state. It is a good idea to always make initial state so we can be confident what we will get when we start subscribing to the store before any actions that might update the state are dispatched. 13 | 14 | * Add an interface and initial state to our spinner reducer. 15 | 16 | {% code-tabs %} 17 | {% code-tabs-item title="src/app/state/spinner.reducer.ts" %} 18 | ```typescript 19 | export interface State { 20 | isOn: boolean; 21 | } 22 | 23 | export const initialState: State = { 24 | isOn: false 25 | }; 26 | 27 | export function reducer(state = initialState, action): State { 28 | switch (action.type) { 29 | case 'startSpinner': { 30 | return { 31 | isOn: true 32 | }; 33 | } 34 | 35 | case 'stopSpinner': { 36 | return { 37 | isOn: false 38 | }; 39 | } 40 | 41 | default: 42 | return state; 43 | } 44 | } 45 | 46 | ``` 47 | {% endcode-tabs-item %} 48 | {% endcode-tabs %} 49 | 50 | ## 2. Add a global state interface 51 | 52 | This is a difficult task as most applications lazily load the majority of their state, meaning we can only specify the state that is always loaded when the app loads. We can then extend our feature state objects to have these "global" state reducers. 53 | 54 | * Create a state.ts file in our state folder. 55 | * Add a global interface to describe all the global state that will be initialised up front in our application on load. 56 | 57 | {% code-tabs %} 58 | {% code-tabs-item title="src/app/state/state.ts" %} 59 | ```typescript 60 | import * as fromSpinner from './spinner/spinner.reducer'; 61 | 62 | export interface State { 63 | spinner: fromSpinner.State; 64 | } 65 | 66 | ``` 67 | {% endcode-tabs-item %} 68 | {% endcode-tabs %} 69 | 70 | ## 3. Use global interface in EventComponent 71 | 72 | Now we can inject the Store into our components with a type. This is useful to say what state reducers we should be able to interact with from this component but in reality the store is a global object and these types do not stop us getting the whole store. 73 | 74 | * Update the store to use the new State interface. 75 | 76 | {% code-tabs %} 77 | {% code-tabs-item title="src/app/event/container/event/event.component.ts" %} 78 | ```typescript 79 | ----------- ABBREVIATED CODE SNIPPPET ---------- 80 | 81 | constructor( 82 | private store: Store, 83 | private eventService: EventService 84 | ) {} 85 | 86 | 87 | ----------- ABBREVIATED CODE SNIPPPET ---------- 88 | ``` 89 | {% endcode-tabs-item %} 90 | {% endcode-tabs %} 91 | 92 | ## 4. Create action creators 93 | 94 | Action creators are a big part of making an Angular application that uses NgRx more robust. It helps get type inference and code hints in our app and stops users dispatching actions with mistakes or the wrong actions with maybe the wrong payloads. 95 | 96 | The `SpinnerActionTypes` enum is useful for having a strongly typed list of the actions we can dispatch. It is also handy to have detail in them so when we see them in our dev tools we can also see the place and time they are dispatched from, making debugging easier when we are less familiar with the code written by other or even ourselves. 97 | 98 | * Create a spinner.actions.ts file in our _state/spinner_ folder. 99 | * Make action creators to strongly type our actions. 100 | 101 | {% code-tabs %} 102 | {% code-tabs-item title="src/app/state/spinner/spinner.actions.ts" %} 103 | ```typescript 104 | import { Action } from '@ngrx/store'; 105 | 106 | export enum SpinnerActionTypes { 107 | StartSpinner = '[Spinner Page] Start Spinner', 108 | StopSpinner = '[Spinner Page] Stop Spinner' 109 | } 110 | 111 | export class StartSpinner implements Action { 112 | readonly type = SpinnerActionTypes.StartSpinner; 113 | } 114 | 115 | export class StopSpinner implements Action { 116 | readonly type = SpinnerActionTypes.StopSpinner; 117 | } 118 | 119 | export type SpinnerActions = StopSpinner | StartSpinner; 120 | 121 | ``` 122 | {% endcode-tabs-item %} 123 | {% endcode-tabs %} 124 | 125 | ## 5. Use action creators in our components 126 | 127 | * Use action creators in EventComponent and remove our un typed actions we are dispatching. 128 | 129 | {% code-tabs %} 130 | {% code-tabs-item title="src/app/event/event.component.ts" %} 131 | ```typescript 132 | import { Component, OnInit } from '@angular/core'; 133 | import { Observable } from 'rxjs'; 134 | import { Store, select } from '@ngrx/store'; 135 | 136 | import { Attendee } from '../../../models'; 137 | import { EventService } from '../../services/event.service'; 138 | import { State } from '../../../state/state'; 139 | import { StartSpinner, StopSpinner } from '../../../state/spinner/spinner.actions'; 140 | 141 | @Component({ 142 | selector: 'app-event', 143 | templateUrl: './event.component.html', 144 | styleUrls: ['./event.component.scss'] 145 | }) 146 | export class EventComponent implements OnInit { 147 | spinner$: Observable; 148 | attendees$: Observable; 149 | 150 | constructor( 151 | private store: Store, 152 | private eventService: EventService 153 | ) {} 154 | 155 | ngOnInit() { 156 | this.getAttendees(); 157 | this.spinner$ = this.store.pipe(select(state => state.spinner.isOn)); 158 | } 159 | 160 | getAttendees() { 161 | this.attendees$ = this.eventService.getAttendees(); 162 | } 163 | 164 | addAttendee(attendee: Attendee) { 165 | this.store.dispatch(new StartSpinner()); 166 | this.eventService.addAttendee(attendee).subscribe(() => { 167 | this.store.dispatch(new StopSpinner()); 168 | this.getAttendees(); 169 | }); 170 | } 171 | } 172 | 173 | ``` 174 | {% endcode-tabs-item %} 175 | {% endcode-tabs %} 176 | 177 | ## 6. Use ActionTypes and Actions type in reducer 178 | 179 | We will break our unit tests by using our `SpinnerActionTypes` which now have a different string and words for the type argument of our actions but we will fix these in the next section. 180 | 181 | * Type the action argument passed into our reducer function as `SpinnerActions`. 182 | * Change our case statements to use our new `SpinnerActionTypes`. 183 | 184 | {% code-tabs %} 185 | {% code-tabs-item title="src/app/state/spinner.reducer.ts" %} 186 | ```typescript 187 | import { SpinnerActionTypes, SpinnerActions } from './spinner.actions'; 188 | 189 | export interface State { 190 | isOn: boolean; 191 | } 192 | 193 | export const initialState: State = { 194 | isOn: false 195 | }; 196 | 197 | export function reducer(state = initialState, action: SpinnerActions): State { 198 | switch (action.type) { 199 | case SpinnerActionTypes.StartSpinner: { 200 | return { 201 | isOn: true 202 | }; 203 | } 204 | 205 | case SpinnerActionTypes.StopSpinner: { 206 | return { 207 | isOn: false 208 | }; 209 | } 210 | 211 | default: 212 | return state; 213 | } 214 | } 215 | 216 | ``` 217 | {% endcode-tabs-item %} 218 | {% endcode-tabs %} 219 | 220 | ## StackBlitz Link 221 | 222 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/15-strongly-type-ngrx\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 223 | 224 | 225 | 226 | ## Extras and Homework 227 | 228 | {% hint style="danger" %} 229 | These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises. 230 | 231 | WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course. 232 | 233 | You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you. 234 | {% endhint %} 235 | 236 | ### Learn more about Action Hygiene from the NgRx team in this ngConf you tube video 237 | 238 | {% embed data="{\"url\":\"https://www.youtube.com/watch?v=JmnsEvoy-gY\",\"type\":\"video\",\"title\":\"Good Action Hygiene with NgRx Mike Ryan\",\"description\":\"ng-conf is a two day, single track conference focused on delivering the highest quality training in the Angular JavaScript framework. 1500+ developers from across the country will converge on beautiful Salt Lake City, UT to participate in training sessions by the Google Angular team, and other Angular experts. In addition to the invaluable training, ng-conf will deliver a premier conference experience for attendees, providing opportunities to network with other developers, relax at social events, and engage in some of the unique entertainment opportunities available in Utah.\",\"icon\":{\"type\":\"icon\",\"url\":\"https://www.youtube.com/yts/img/favicon\_144-vfliLAfaB.png\",\"width\":144,\"height\":144,\"aspectRatio\":1},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://i.ytimg.com/vi/JmnsEvoy-gY/maxresdefault.jpg\",\"width\":1280,\"height\":720,\"aspectRatio\":0.5625},\"embed\":{\"type\":\"player\",\"url\":\"https://www.youtube.com/embed/JmnsEvoy-gY?rel=0&showinfo=0\",\"html\":\"
\",\"aspectRatio\":1.7778}}" %} 239 | 240 | Steps: 241 | 242 | 1. Watch the video 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /11.-create-eventservice.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: In this section we will add a service and discuss dependency injection. 3 | --- 4 | 5 | # 11. Create EventService 6 | 7 | ## [Link to Section Slides](https://docs.google.com/presentation/d/1Y7Tf7kjO4Li0ihhkVgRjn4szFJPAkbMvilfrDCbrjq8/edit#slide=id.g2fa7fd70ec_0_536) 8 | 9 | ## 1. Add an Angular service and return an observable of Attendee 10 | 11 | * Make an Angular service. 12 | * Run the following command to make a service. 13 | 14 | ```text 15 | ng g service event/services/event 16 | ``` 17 | 18 | ## 2. Add logic to the service 19 | 20 | * Add logic to the `EventService` to return a hardcoded `Attendee` observable array. 21 | 22 | {% code-tabs %} 23 | {% code-tabs-item title="src/app/event/services/event.service.ts" %} 24 | ```typescript 25 | import { Injectable } from '@angular/core'; 26 | import { Attendee } from '../../models'; 27 | import { Observable, of } from 'rxjs'; 28 | 29 | @Injectable({ 30 | providedIn: 'root' 31 | }) 32 | export class EventService { 33 | constructor() {} 34 | 35 | getAttendees(): Observable { 36 | return of([ 37 | { 38 | name: 'Duncan', 39 | attending: true, 40 | guests: 0 41 | } 42 | ] as Attendee[]); 43 | } 44 | } 45 | ``` 46 | {% endcode-tabs-item %} 47 | {% endcode-tabs %} 48 | 49 | ## 3. Inject service into the component 50 | 51 | * Inject and use the new service in the component. 52 | * Subscribe to the observable of Attendee returned from the service. 53 | * Add a getAttendees method to the component. 54 | 55 | {% code-tabs %} 56 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 57 | ```typescript 58 | import { Component, OnInit } from '@angular/core'; 59 | import { Attendee } from '../../../models'; 60 | import { EventService } from '../../services/event.service'; 61 | 62 | @Component({ 63 | selector: 'app-event', 64 | templateUrl: './event.component.html', 65 | styleUrls: ['./event.component.scss'] 66 | }) 67 | export class EventComponent implements OnInit { 68 | attendees: Attendee[] = []; 69 | constructor(private eventService: EventService) {} 70 | 71 | ngOnInit() { 72 | this.getAttendees(); 73 | } 74 | 75 | getAttendees() { 76 | this.eventService 77 | .getAttendees() 78 | .subscribe(attendees => (this.attendees = attendees)); 79 | } 80 | 81 | addAttendee(attendee: Attendee) { 82 | this.attendees = [...this.attendees, attendee]; 83 | console.log( 84 | 'TCL: EventComponent -> addAttendee -> this.attendees', 85 | this.attendees 86 | ); 87 | } 88 | } 89 | ``` 90 | {% endcode-tabs-item %} 91 | {% endcode-tabs %} 92 | 93 | ## 4. Swap from subscription to async pipe 94 | 95 | Angular [pipes](https://angular.io/guide/pipes), a way to write display-value transformations that you can declare in your HTML. The async pipe is a special built in pipe from Angular that will subscribe to an observable for you in the HTML template and also unsubscribe when the components `ngOnDestory` life cycle hook is fired. Another quirk is that it will also mark the component to be checked by Angular's change detection on its next cycle. 96 | 97 | * Use async pipe versus a subscription to get the attendees from the observable. 98 | 99 | {% code-tabs %} 100 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 101 | ```typescript 102 | import { Component, OnInit } from '@angular/core'; 103 | import { Attendee } from '../../../models'; 104 | import { EventService } from '../../services/event.service'; 105 | import { Observable } from 'rxjs'; 106 | 107 | @Component({ 108 | selector: 'app-event', 109 | templateUrl: './event.component.html', 110 | styleUrls: ['./event.component.scss'] 111 | }) 112 | export class EventComponent implements OnInit { 113 | attendees$: Observable; 114 | 115 | constructor(private eventService: EventService) {} 116 | 117 | ngOnInit() { 118 | this.getAttendees(); 119 | } 120 | 121 | getAttendees() { 122 | this.attendees$ = this.eventService.getAttendees(); 123 | } 124 | } 125 | ``` 126 | {% endcode-tabs-item %} 127 | {% endcode-tabs %} 128 | 129 | ## 5. Use async pipe in HTML 130 | 131 | * Swap out attendee for `attendee$ | async` in the HTML. We can leave the original attendees property alone for now. 132 | 133 | {% code-tabs %} 134 | {% code-tabs-item title="src/app/event/containers/event/event.component.html" %} 135 | ```markup 136 | 137 | 138 | ``` 139 | {% endcode-tabs-item %} 140 | {% endcode-tabs %} 141 | 142 | ## 6. Add fake backend 143 | 144 | Angular's own `HttpClientInMemoryWebApiModule` can help us make a little mock server without needing to add a real server set up. You can read more about how it works here [https://github.com/angular/in-memory-web-api](https://github.com/angular/in-memory-web-api). 145 | 146 | * Add fake backend with with npm. 147 | 148 | ```text 149 | npm i angular-in-memory-web-api -D 150 | ``` 151 | 152 | * Add `HttpClientModule` 153 | * Add the `HttpClientInMemoryWebApiModule` to the imports array of the `@ngModule` and register the `InMemoryDataService` we will write in the next step. 154 | 155 | {% code-tabs %} 156 | {% code-tabs-item title="src/app/app.module.ts" %} 157 | ```typescript 158 | import { BrowserModule } from '@angular/platform-browser'; 159 | import { NgModule } from '@angular/core'; 160 | import { HttpClientModule } from '@angular/common/http'; 161 | import { RouterModule } from '@angular/router'; 162 | 163 | import { AppComponent } from './app.component'; 164 | import { HomeComponent } from './home/containers/home/home.component'; 165 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 166 | import { InMemoryDataService } from './app.db'; 167 | @NgModule({ 168 | declarations: [AppComponent, HomeComponent], 169 | imports: [ 170 | BrowserModule, 171 | RouterModule.forRoot([ 172 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 173 | { path: 'home', component: HomeComponent }, 174 | { path: 'event', loadChildren: './event/event.module#EventModule' } 175 | ]), 176 | HttpClientModule, 177 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { delay: 100 }), 178 | ], 179 | providers: [], 180 | bootstrap: [AppComponent] 181 | }) 182 | export class AppModule {} 183 | ``` 184 | {% endcode-tabs-item %} 185 | {% endcode-tabs %} 186 | 187 | ## 7. Make the InMemoryDataService 188 | 189 | * Create the missing `InMemoryDataService` in the src/app folder. Do not worry too much about learning how this works we just need a little fake backend so we can focus on understanding Angular and NgRx. 190 | 191 | {% code-tabs %} 192 | {% code-tabs-item title="src/app/app.db.ts" %} 193 | ```typescript 194 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 195 | import { Attendee } from './models'; 196 | 197 | export class InMemoryDataService implements InMemoryDbService { 198 | createDb() { 199 | const attendees = [ 200 | { 201 | id: 1, 202 | name: 'Duncan In Memory', 203 | attending: true, 204 | guests: 0 205 | } 206 | ] as Attendee[]; 207 | return { attendees }; 208 | } 209 | } 210 | ``` 211 | {% endcode-tabs-item %} 212 | {% endcode-tabs %} 213 | 214 | ## 8. Update Attendee interface to have an optional Id 215 | 216 | * Add optional id to Attendee interface with a `id?: number` syntax. 217 | 218 | {% code-tabs %} 219 | {% code-tabs-item title="src/app/models/attendee.ts" %} 220 | ```typescript 221 | export interface Attendee { 222 | id?: number; 223 | name: string; 224 | attending: boolean; 225 | guests: number; 226 | } 227 | ``` 228 | {% endcode-tabs-item %} 229 | {% endcode-tabs %} 230 | 231 | ## 9. Add HttpClientModule to EventModule 232 | 233 | Angular modules describe the dependencies for this section of code so we will need to also add the HttpClientModule here to. The build tool is smart enough to know we registered it in the root module so we will not pay for it twice in the browser bundled JavaScript. 234 | 235 | * Add `HttpClientModule` to the EventModule. 236 | 237 | {% code-tabs %} 238 | {% code-tabs-item title="src/app/event/event.module.ts" %} 239 | ```typescript 240 | import { NgModule } from '@angular/core'; 241 | import { CommonModule } from '@angular/common'; 242 | import { RouterModule } from '@angular/router'; 243 | import { ReactiveFormsModule } from '@angular/forms'; 244 | import { HttpClientModule } from '@angular/common/http'; 245 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 246 | 247 | import { EventComponent } from './containers/event/event.component'; 248 | import { AddAttendeeComponent } from './components/add-attendee/add-attendee.component'; 249 | import { EventListComponent } from './components/event-list/event-list.component'; 250 | @NgModule({ 251 | imports: [ 252 | CommonModule, 253 | RouterModule.forChild([{ path: '', component: EventComponent }]), 254 | ReactiveFormsModule, 255 | HttpClientModule 256 | ], 257 | declarations: [EventComponent, AddAttendeeComponent, EventListComponent] 258 | }) 259 | export class EventModule {} 260 | ``` 261 | {% endcode-tabs-item %} 262 | {% endcode-tabs %} 263 | 264 | ## 10. Update service to call fake endpoint 265 | 266 | * Update the service by injecting the `httpClient` into the constructor. 267 | * Change the `getAttendee` method to use the `httpClient` and fake backend. 268 | * Add an `addAttendee` method to save the added attendees. 269 | 270 | {% code-tabs %} 271 | {% code-tabs-item title="src/app/event/event.service.ts" %} 272 | ```typescript 273 | import { Injectable } from '@angular/core'; 274 | import { Attendee } from '../../models'; 275 | import { Observable, of } from 'rxjs'; 276 | import { HttpClient } from '@angular/common/http'; 277 | 278 | @Injectable({ 279 | providedIn: 'root' 280 | }) 281 | export class EventService { 282 | constructor(private httpClient: HttpClient) {} 283 | 284 | getAttendees(): Observable { 285 | return this.httpClient.get('/api/attendees'); 286 | } 287 | 288 | addAttendee(attendee: Attendee): Observable { 289 | return this.httpClient.post('/api/attendees', attendee); 290 | } 291 | } 292 | ``` 293 | {% endcode-tabs-item %} 294 | {% endcode-tabs %} 295 | 296 | ## 12. Update container component to have an add attendee method 297 | 298 | * Add new method to call `addAttendee` on the service and then call `getAttendees` after it saves. 299 | 300 | {% code-tabs %} 301 | {% code-tabs-item title="src\\app\\event\\containers\\event\\event.component.ts" %} 302 | ```typescript 303 | import { Component, OnInit } from '@angular/core'; 304 | import { Attendee } from '../../../models'; 305 | import { EventService } from '../../services/event.service'; 306 | import { Observable } from 'rxjs'; 307 | 308 | @Component({ 309 | selector: 'app-event', 310 | templateUrl: './event.component.html', 311 | styleUrls: ['./event.component.scss'] 312 | }) 313 | export class EventComponent implements OnInit { 314 | attendees$: Observable; 315 | 316 | constructor(private eventService: EventService) {} 317 | 318 | ngOnInit() { 319 | this.getAttendees(); 320 | } 321 | 322 | getAttendees() { 323 | this.attendees$ = this.eventService.getAttendees(); 324 | } 325 | 326 | addAttendee(attendee: Attendee) { 327 | this.eventService 328 | .addAttendee(attendee) 329 | .subscribe(() => this.getAttendees()); 330 | } 331 | } 332 | ``` 333 | {% endcode-tabs-item %} 334 | {% endcode-tabs %} 335 | 336 | ## StackBlitz Link 337 | 338 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/11-create-event-service\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 339 | 340 | -------------------------------------------------------------------------------- /24.-router-store.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will use NgRx's router store library to listen to url or 4 | router events as actions in our effects. 5 | --- 6 | 7 | # 24. Router store 8 | 9 | ## 1. npm i @ngrx/router-store 10 | 11 | ```text 12 | npm i @ngrx/router-store 13 | ``` 14 | 15 | ## 2. Add to AppModule 16 | 17 | * Add `StoreRouterConnectingModule` to the AppModule. 18 | * Often it is needed to create your own router serilaizer but we will skip this step. 19 | 20 | {% code-tabs %} 21 | {% code-tabs-item title="src/app/app.module.ts" %} 22 | ```typescript 23 | import { BrowserModule } from '@angular/platform-browser'; 24 | import { NgModule } from '@angular/core'; 25 | import { HttpClientModule } from '@angular/common/http'; 26 | import { RouterModule } from '@angular/router'; 27 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 28 | import { StoreModule } from '@ngrx/store'; 29 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 30 | import { EffectsModule } from '@ngrx/effects'; 31 | import { StoreRouterConnectingModule } from '@ngrx/router-store'; 32 | 33 | import { HomeComponent } from './home/containers/home/home.component'; 34 | import { AppComponent } from './app.component'; 35 | import { InMemoryDataService } from './app.db'; 36 | import { reducer } from './state/spinner/spinner.reducer'; 37 | import { environment } from '../environments/environment.prod'; 38 | 39 | @NgModule({ 40 | declarations: [AppComponent, HomeComponent], 41 | imports: [ 42 | BrowserModule, 43 | RouterModule.forRoot([ 44 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 45 | { path: 'home', component: HomeComponent }, 46 | { path: 'event', loadChildren: './event/event.module#EventModule' } 47 | ]), 48 | HttpClientModule, 49 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { 50 | delay: 1000 51 | }), 52 | StoreModule.forRoot({ spinner: reducer }), 53 | EffectsModule.forRoot([]), 54 | StoreDevtoolsModule.instrument({ 55 | name: 'NgRx Demo App', 56 | logOnly: environment.production 57 | }), 58 | StoreRouterConnectingModule.forRoot(), 59 | ], 60 | providers: [], 61 | bootstrap: [AppComponent] 62 | }) 63 | export class AppModule {} 64 | 65 | 66 | ``` 67 | {% endcode-tabs-item %} 68 | {% endcode-tabs %} 69 | 70 | ![Image: Showing router events as actions in the devtools](.gitbook/assets/image%20%2817%29.png) 71 | 72 | ## 3. Add a select box to EventComponent to update URL 73 | 74 | * Add select box element. 75 | 76 | {% code-tabs %} 77 | {% code-tabs-item title="src/app/event/event.component.html" %} 78 | ```markup 79 | 80 | 85 | 86 | 87 | 88 | ``` 89 | {% endcode-tabs-item %} 90 | {% endcode-tabs %} 91 | 92 | ## 4. Add navigate method to component 93 | 94 | * Add a navigate method to the component to update the URL. 95 | * Delete dispatched LoadAttendees action. 96 | 97 | {% code-tabs %} 98 | {% code-tabs-item title="src/app/event/event.component.ts" %} 99 | ```typescript 100 | import { Component, OnInit } from '@angular/core'; 101 | import { Observable } from 'rxjs'; 102 | import { Store, select } from '@ngrx/store'; 103 | 104 | import { Attendee } from '../../../models'; 105 | import { EventService } from '../../services/event.service'; 106 | import { 107 | StartSpinner, 108 | StopSpinner 109 | } from '../../../state/spinner/spinner.actions'; 110 | import { getSpinner } from '../../../state/spinner/spinner.selectors'; 111 | import { 112 | LoadAttendees, 113 | AddAttendee 114 | } from '../../state/attendees/attendees.actions'; 115 | import { EventState } from '../../state'; 116 | import { getAttendees } from '../../state/attendees/attendees.selectors'; 117 | import { Router } from '@angular/router'; 118 | 119 | @Component({ 120 | selector: 'app-event', 121 | templateUrl: './event.component.html', 122 | styleUrls: ['./event.component.scss'] 123 | }) 124 | export class EventComponent implements OnInit { 125 | spinner$: Observable; 126 | attendees$: Observable; 127 | 128 | constructor( 129 | private store: Store, 130 | private eventService: EventService, 131 | private router: Router 132 | ) {} 133 | 134 | ngOnInit() { 135 | this.attendees$ = this.store.pipe(select(getAttendees)); 136 | } 137 | 138 | addAttendee(attendee: Attendee) { 139 | this.store.dispatch(new AddAttendee(attendee)); 140 | } 141 | 142 | navigate(filterBy: string) { 143 | this.router.navigateByUrl(`/event?filterBy=${filterBy}`); 144 | } 145 | } 146 | 147 | ``` 148 | {% endcode-tabs-item %} 149 | {% endcode-tabs %} 150 | 151 | ## 5. Add next action creator for filterBy property 152 | 153 | * Add new `AttendeesActionTypes` for `FilterBy`. 154 | * Add new class for `FilterBy` 155 | * Add new `AttendeesActions` for `FilterBy`. 156 | 157 | {% code-tabs %} 158 | {% code-tabs-item title="src/app/event/state/attendees/attendees.actions.ts" %} 159 | ```typescript 160 | import { Action } from '@ngrx/store'; 161 | import { Attendee } from '../../../models'; 162 | export enum AttendeesActionTypes { 163 | LoadAttendees = '[Attendees Page] Load Attendees', 164 | LoadAttendeesSuccess = '[Attendees Page] Load Attendees Success', 165 | LoadAttendeesFail = '[Attendees Page] Load Attendees Fail', 166 | AddAttendee = '[Attendee Page] Add Attendee', 167 | AddAttendeeSuccess = '[Attendee API] Add Attendee Success', 168 | AddAttendeeFail = '[Attendee API] Add Attendee Fail', 169 | FilterBy = '[Attendee Page] FilterBy' 170 | } 171 | export class LoadAttendees implements Action { 172 | readonly type = AttendeesActionTypes.LoadAttendees; 173 | } 174 | export class LoadAttendeesSuccess implements Action { 175 | readonly type = AttendeesActionTypes.LoadAttendeesSuccess; 176 | constructor(public payload: Attendee[]) {} 177 | } 178 | export class LoadAttendeesFail implements Action { 179 | readonly type = AttendeesActionTypes.LoadAttendeesFail; 180 | constructor(public payload: any) {} 181 | } 182 | 183 | export class AddAttendee implements Action { 184 | readonly type = AttendeesActionTypes.AddAttendee; 185 | constructor(public payload: Attendee) {} 186 | } 187 | 188 | export class AddAttendeeSuccess implements Action { 189 | readonly type = AttendeesActionTypes.AddAttendeeSuccess; 190 | constructor(public payload: Attendee) {} 191 | } 192 | 193 | export class AddAttendeeFail implements Action { 194 | readonly type = AttendeesActionTypes.AddAttendeeFail; 195 | constructor(public payload: any) {} 196 | } 197 | 198 | export class FilterBy implements Action { 199 | readonly type = AttendeesActionTypes.FilterBy; 200 | constructor(public payload: string) {} 201 | } 202 | 203 | export type AttendeesActions = 204 | | FilterBy 205 | | AddAttendee 206 | | AddAttendeeSuccess 207 | | AddAttendeeFail 208 | | LoadAttendees 209 | | LoadAttendeesSuccess 210 | | LoadAttendeesFail; 211 | 212 | ``` 213 | {% endcode-tabs-item %} 214 | {% endcode-tabs %} 215 | 216 | ## 6. Add an effect to listen to router events 217 | 218 | * Add an effect listening to `RouterEvents` that begin with `/events`. 219 | 220 | {% code-tabs %} 221 | {% code-tabs-item title="src/app/event/state/attendees.effects.ts" %} 222 | ```typescript 223 | import { Injectable } from '@angular/core'; 224 | import { Actions, Effect } from '@ngrx/effects'; 225 | import { ofType } from '@ngrx/effects'; 226 | import { switchMap, map, catchError, filter, tap } from 'rxjs/operators'; 227 | import { of } from 'rxjs'; 228 | 229 | import { EventService } from '../../services/event.service'; 230 | import { 231 | AttendeesActionTypes, 232 | LoadAttendees, 233 | LoadAttendeesSuccess, 234 | LoadAttendeesFail, 235 | AddAttendee, 236 | AddAttendeeSuccess, 237 | AddAttendeeFail, 238 | FilterBy 239 | } from './attendees.actions'; 240 | import { Attendee } from '../../../models'; 241 | import { ROUTER_NAVIGATION } from '@ngrx/router-store'; 242 | import { RouterNavigationAction } from '@ngrx/router-store'; 243 | 244 | @Injectable() 245 | export class AttendeesEffects { 246 | constructor(private actions$: Actions, private eventService: EventService) {} 247 | 248 | @Effect() 249 | getAttendees$ = this.actions$.pipe( 250 | ofType(AttendeesActionTypes.LoadAttendees), 251 | switchMap((action: LoadAttendees) => 252 | this.eventService.getAttendees().pipe( 253 | map((attendees: Attendee[]) => new LoadAttendeesSuccess(attendees)), 254 | catchError(error => of(new LoadAttendeesFail(error))) 255 | ) 256 | ) 257 | ); 258 | 259 | @Effect() 260 | addAttendee$ = this.actions$.pipe( 261 | ofType(AttendeesActionTypes.AddAttendee), 262 | switchMap((action: AddAttendee) => 263 | this.eventService.addAttendee(action.payload).pipe( 264 | map((attendee: Attendee) => new AddAttendeeSuccess(attendee)), 265 | catchError(error => of(new AddAttendeeFail(error))) 266 | ) 267 | ) 268 | ); 269 | 270 | @Effect() 271 | loadDiaryHealthActions$ = this.actions$.pipe( 272 | ofType(ROUTER_NAVIGATION), 273 | map((r: RouterNavigationAction) => ({ 274 | url: r.payload.routerState.url, 275 | filterBy: r.payload.routerState.root.queryParams['filterBy'] 276 | })), 277 | filter(({ url, filterBy }) => url.startsWith('/event')), 278 | map(({ filterBy }) => new FilterBy(filterBy)) 279 | ); 280 | } 281 | 282 | ``` 283 | {% endcode-tabs-item %} 284 | {% endcode-tabs %} 285 | 286 | ## 7. Update reducer to have new filterBy 287 | 288 | * Add switch case to set filterBy property. 289 | 290 | {% code-tabs %} 291 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 292 | ```typescript 293 | import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; 294 | 295 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 296 | import { Attendee } from '../../../models'; 297 | 298 | export interface State extends EntityState { 299 | loading: boolean; 300 | error: any; 301 | filterBy: string; 302 | } 303 | 304 | const adapter: EntityAdapter = createEntityAdapter(); 305 | 306 | export const intitalState: State = adapter.getInitialState({ 307 | loading: false, 308 | error: null, 309 | filterBy: 'all' 310 | }); 311 | 312 | export function reducer(state = intitalState, action: AttendeesActions): State { 313 | switch (action.type) { 314 | case AttendeesActionTypes.LoadAttendees: { 315 | return adapter.removeAll({ 316 | ...state, 317 | loading: false, 318 | error: null 319 | }); 320 | } 321 | 322 | case AttendeesActionTypes.LoadAttendeesSuccess: { 323 | return adapter.addAll(action.payload, { 324 | ...state, 325 | loading: false, 326 | error: null 327 | }); 328 | } 329 | 330 | case AttendeesActionTypes.LoadAttendeesFail: { 331 | return adapter.removeAll({ 332 | ...state, 333 | loading: false, 334 | error: action.payload 335 | }); 336 | } 337 | 338 | case AttendeesActionTypes.AddAttendeeSuccess: { 339 | return adapter.addOne(action.payload, { ...state, error: null }); 340 | } 341 | 342 | case AttendeesActionTypes.AddAttendeeFail: { 343 | return { ...state, error: action.payload }; 344 | } 345 | 346 | case AttendeesActionTypes.FilterBy: { 347 | return { ...state, filterBy: action.payload }; 348 | } 349 | 350 | default: { 351 | return state; 352 | } 353 | } 354 | } 355 | 356 | export const { 357 | selectIds, 358 | selectEntities, 359 | selectAll, 360 | selectTotal 361 | } = adapter.getSelectors(); 362 | 363 | ``` 364 | {% endcode-tabs-item %} 365 | {% endcode-tabs %} 366 | 367 | ## 8. Add new selector for filtering attendees. 368 | 369 | * Add selector to get filterBY state property. 370 | * Use two getAttendees and getFilterBy to filter the list. 371 | 372 | {% code-tabs %} 373 | {% code-tabs-item title="src/app/event/state/attendees/attendees.selectors.ts" %} 374 | ```typescript 375 | ---------- ABBREVIATED CODE SNIPPET ---------- 376 | 377 | export const getFilterBy = createSelector( 378 | getAttendeeState, 379 | state => state.filterBy 380 | ); 381 | 382 | export const getFilteredAttendees = createSelector( 383 | getAttendees, 384 | getFilterBy, 385 | (attendees, filterBy) => 386 | attendees.filter( 387 | attendee => 388 | filterBy === 'all' 389 | ? true 390 | : filterBy === 'withGuests' 391 | ? attendee.guests >= 1 392 | : attendee.guests === 0 393 | ) 394 | ); 395 | 396 | ---------- ABBREVIATED CODE SNIPPET ---------- 397 | ``` 398 | {% endcode-tabs-item %} 399 | {% endcode-tabs %} 400 | 401 | ## 9. Use new selector in EventComponent 402 | 403 | * Swap getAttendees selector for getFilteredAttendees selector. 404 | 405 | {% code-tabs %} 406 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 407 | ```typescript 408 | ---------- ABBREVIATED CODE SNIPPET ---------- 409 | 410 | ngOnInit() { 411 | this.attendees$ = this.store.pipe(select(getFilteredAttendees)); 412 | this.store.dispatch(new LoadAttendees()); 413 | } 414 | 415 | ---------- ABBREVIATED CODE SNIPPET ---------- 416 | ``` 417 | {% endcode-tabs-item %} 418 | {% endcode-tabs %} 419 | 420 | ## StackBlitz Link 421 | 422 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/24-router-store\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 423 | 424 | 425 | 426 | -------------------------------------------------------------------------------- /20.-create-effect.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: >- 3 | In this section we will discuss NgRx's effects library a RxJS powered side 4 | effect model for @ngrx/store. 5 | --- 6 | 7 | # 20. Create effect 8 | 9 | @ngrx/effects provides an API to model event sources as actions. Effects: 10 | 11 | 1. Listen for actions dispatched from @ngrx/store. 12 | 2. Isolate side effects from components, allowing for more _pure_ components that select state and dispatch actions. 13 | 3. Provide [new sources](https://martinfowler.com/eaaDev/EventSourcing.html) of actions to reduce state based on external interactions such as network requests, web socket messages and time-based events. 14 | 15 | ## 1. npm install @ngrx/effects 16 | 17 | * Execute the following command to install the effects library. 18 | 19 | ```text 20 | npm i @ngrx/effects 21 | ``` 22 | 23 | ## 2. Dispatch new LoadAttendees action from the EventComponent 24 | 25 | Before we can use effect we need to dispatch some actions from our EventComponent for our effects to listen to. We will not need the service in our component when we finish but for now we still need it to add Attendees. 26 | 27 | * Dispatch actions to `LoadAttendees`. 28 | * Use the new `EventState` to limit the auto completion of what we are meant to be able to access from the store. Note we can still access everything as the store is a global object. 29 | * Remove getAttendees method which will now be handled by the effect we are about to write. 30 | 31 | {% code-tabs %} 32 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 33 | ```typescript 34 | import { Component, OnInit } from '@angular/core'; 35 | import { Observable } from 'rxjs'; 36 | import { Store, select } from '@ngrx/store'; 37 | 38 | import { Attendee } from '../../../models'; 39 | import { EventService } from '../../services/event.service'; 40 | import { StartSpinner, StopSpinner } from '../../../state/spinner/spinner.actions'; 41 | import { getSpinner } from '../../../state/spinner/spinner.selectors'; 42 | import { LoadAttendees } from '../../state/attendees/attendees.actions'; 43 | import { State } from '../../state'; 44 | 45 | @Component({ 46 | selector: 'app-event', 47 | templateUrl: './event.component.html', 48 | styleUrls: ['./event.component.scss'] 49 | }) 50 | export class EventComponent implements OnInit { 51 | spinner$: Observable; 52 | attendees$: Observable; 53 | 54 | constructor( 55 | private store: Store, 56 | private eventService: EventService 57 | ) {} 58 | 59 | ngOnInit() { 60 | this.store.dispatch(new LoadAttendees()); 61 | this.spinner$ = this.store.pipe(select(getSpinner)); 62 | this.attendees$ = this.store.pipe(select(state => state.event.attendees.attendees)); 63 | } 64 | 65 | addAttendee(attendee: Attendee) { 66 | this.store.dispatch(new StartSpinner()); 67 | this.eventService.addAttendee(attendee).subscribe(() => { 68 | this.store.dispatch(new StopSpinner()); 69 | }); 70 | } 71 | } 72 | 73 | ``` 74 | {% endcode-tabs-item %} 75 | {% endcode-tabs %} 76 | 77 | ## 3. Create an effect 78 | 79 | Effects are all about listening for actions doing work and dispatching new actions. So in our example we will listen for the `LoadAttendees` action do the work of getting them via a service and then dispatch a `LoadAttendeesSuccess` action. 80 | 81 | NgRx effects are a deep dive into observables and if you have not done a lot with RxJS or reactive streams in other languages can take some time to step through. 82 | 83 | * Create and attendees.effects.ts file in our state/attendees folder path. 84 | * Inject the `actions` observable from NgRx that will emit each action dispatched in our application and the `EventService` to get the attendees. 85 | * Add an `@Effect` decorator on top of a variable name of the effect. 86 | * List the injected actions and filter on them with the `ofType` operator from NgRx. 87 | * Use the `switchMap` operator to switch from the actions stream to a new observable returned from our EventService and return an `LoadAttendeesSuccess` action. 88 | * Add a `catchError` operator and return an observable of `LoadAttendeesFail` with a payload of the error. 89 | 90 | {% code-tabs %} 91 | {% code-tabs-item title="src/app/event/state/attendees/attendees.effects.ts" %} 92 | ```typescript 93 | import { Injectable } from '@angular/core'; 94 | import { Actions, Effect } from '@ngrx/effects'; 95 | import { ofType } from '@ngrx/effects'; 96 | import { switchMap, map, catchError } from 'rxjs/operators'; 97 | import { of } from 'rxjs'; 98 | 99 | import { EventService } from '../../services/event.service'; 100 | import { 101 | AttendeesActionTypes, 102 | LoadAttendees, 103 | LoadAttendeesSuccess, 104 | LoadAttendeesFail 105 | } from './attendees.actions'; 106 | import { Attendee } from '../../../models'; 107 | 108 | @Injectable() 109 | export class AttendeesEffects { 110 | constructor(private actions$: Actions, private eventService: EventService) {} 111 | 112 | @Effect() 113 | getAttendees$ = this.actions$.pipe( 114 | ofType(AttendeesActionTypes.LoadAttendees), 115 | switchMap((action: LoadAttendees) => 116 | this.eventService.getAttendees().pipe( 117 | map((attendees: Attendee[]) => new LoadAttendeesSuccess(attendees)), 118 | catchError(error => of(new LoadAttendeesFail(error))) 119 | ) 120 | ) 121 | ); 122 | } 123 | 124 | ``` 125 | {% endcode-tabs-item %} 126 | {% endcode-tabs %} 127 | 128 | ## 4. Update reducer to listen for success and fail actions 129 | 130 | * Add an error property to the State object to hold any errors. 131 | * Update the attendee reducer to update the store with the attendees array form the affect. 132 | * Add a case for the LoadAttendeesFail action and set its payload to the error property of the state object. 133 | 134 | {% code-tabs %} 135 | {% code-tabs-item title="src/app/event/state/attendees/attendees.reducer.ts" %} 136 | ```typescript 137 | import { Attendee } from '../../../models'; 138 | import { AttendeesActions, AttendeesActionTypes } from './attendees.actions'; 139 | 140 | export interface State { 141 | attendees: Attendee[]; 142 | loading: boolean; 143 | error: any; 144 | } 145 | 146 | export const intitalState: State = { 147 | attendees: [], 148 | loading: false, 149 | error: null 150 | }; 151 | 152 | export function reducer(state = intitalState, action: AttendeesActions): State { 153 | switch (action.type) { 154 | case AttendeesActionTypes.LoadAttendees: { 155 | return { 156 | ...state, 157 | loading: true, 158 | error: null 159 | }; 160 | } 161 | 162 | case AttendeesActionTypes.LoadAttendeesSuccess: { 163 | return { 164 | ...state, 165 | loading: false, 166 | attendees: action.payload, 167 | error: null 168 | }; 169 | } 170 | 171 | case AttendeesActionTypes.LoadAttendeesFail: { 172 | return { 173 | ...state, 174 | loading: false, 175 | error: action.payload 176 | }; 177 | } 178 | 179 | default: { 180 | return state; 181 | } 182 | } 183 | } 184 | 185 | ``` 186 | {% endcode-tabs-item %} 187 | {% endcode-tabs %} 188 | 189 | ## 5. Add effects array to index.ts file for EventModule 190 | 191 | Our index.ts file in our event state folder is our public API so we want to add our different feature states effect to. 192 | 193 | * Re-export effects from the index.ts. 194 | 195 | {% code-tabs %} 196 | {% code-tabs-item title="src/app/event/state/index.ts" %} 197 | ```typescript 198 | import { ActionReducerMap } from '@ngrx/store'; 199 | 200 | import * as fromRoot from './../../state/state'; 201 | import * as fromAttendees from './attendees/attendees.reducer'; 202 | import { AttendeesEffects } from './attendees/attendees.effects'; 203 | 204 | export interface EventState { 205 | attendees: fromAttendees.State; 206 | } 207 | 208 | export interface State extends fromRoot.State { 209 | event: EventState; 210 | } 211 | 212 | export const reducers: ActionReducerMap = { 213 | attendees: fromAttendees.reducer 214 | }; 215 | 216 | export const effects: any[] = [AttendeesEffects]; 217 | ``` 218 | {% endcode-tabs-item %} 219 | {% endcode-tabs %} 220 | 221 | 222 | 223 | ## 6. Register the feature effects in the EventModule 224 | 225 | * Register the effects with the `EffectsModule.forFeature` method. 226 | 227 | {% code-tabs %} 228 | {% code-tabs-item title="src/app/event/event.module.ts" %} 229 | ```typescript 230 | import { NgModule } from '@angular/core'; 231 | import { CommonModule } from '@angular/common'; 232 | import { RouterModule } from '@angular/router'; 233 | import { ReactiveFormsModule } from '@angular/forms'; 234 | import { HttpClientModule } from '@angular/common/http'; 235 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 236 | import { EffectsModule } from '@ngrx/effects'; 237 | import { StoreModule } from '@ngrx/store'; 238 | 239 | import { EventComponent } from './containers/event/event.component'; 240 | import { AddAttendeeComponent } from './components/add-attendee/add-attendee.component'; 241 | import { EventListComponent } from './components/event-list/event-list.component'; 242 | import { reducers, effects } from './state'; 243 | 244 | @NgModule({ 245 | imports: [ 246 | CommonModule, 247 | RouterModule.forChild([{ path: '', component: EventComponent }]), 248 | ReactiveFormsModule, 249 | HttpClientModule, 250 | StoreModule.forFeature('event', reducers), 251 | EffectsModule.forFeature(effects) 252 | ], 253 | declarations: [EventComponent, AddAttendeeComponent, EventListComponent] 254 | }) 255 | export class EventModule {} 256 | 257 | ``` 258 | {% endcode-tabs-item %} 259 | {% endcode-tabs %} 260 | 261 | ## 7. Add default EffectsModule registration to root AppModule 262 | 263 | For us to use effects in our app and reducers we need to have a root reducer and effect to start with. The root reducer could just be an object and the effects an empty array but we need to have them. The feature effects and reducers we make are then added to them as we lazily load them. 264 | 265 | * Add forRoot with an empty array for the AppModule. 266 | 267 | {% code-tabs %} 268 | {% code-tabs-item title="src/app/app.module.ts" %} 269 | ```typescript 270 | import { BrowserModule } from '@angular/platform-browser'; 271 | import { NgModule } from '@angular/core'; 272 | import { HttpClientModule } from '@angular/common/http'; 273 | import { RouterModule } from '@angular/router'; 274 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 275 | import { StoreModule } from '@ngrx/store'; 276 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 277 | import { EffectsModule } from '@ngrx/effects'; 278 | 279 | import { HomeComponent } from './home/containers/home/home.component'; 280 | import { AppComponent } from './app.component'; 281 | import { InMemoryDataService } from './app.db'; 282 | import { reducer } from './state/spinner/spinner.reducer'; 283 | import { environment } from '../environments/environment.prod'; 284 | 285 | @NgModule({ 286 | declarations: [AppComponent, HomeComponent], 287 | imports: [ 288 | BrowserModule, 289 | RouterModule.forRoot([ 290 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 291 | { path: 'home', component: HomeComponent }, 292 | { path: 'event', loadChildren: './event/event.module#EventModule' } 293 | ]), 294 | HttpClientModule, 295 | HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { 296 | delay: 1000 297 | }), 298 | StoreModule.forRoot({ spinner: reducer }), 299 | EffectsModule.forRoot([]), 300 | StoreDevtoolsModule.instrument({ 301 | name: 'NgRx Demo App', 302 | logOnly: environment.production 303 | }) 304 | ], 305 | providers: [], 306 | bootstrap: [AppComponent] 307 | }) 308 | export class AppModule {} 309 | 310 | ``` 311 | {% endcode-tabs-item %} 312 | {% endcode-tabs %} 313 | 314 | * Even before we write selectors we can see our app should working using our new effects. 315 | 316 | ![Image: Redux dev tools showing LoadAttendeesSuccess Action firing](.gitbook/assets/image%20%284%29.png) 317 | 318 | ## 8. Create a getAttendees selector 319 | 320 | Similar to making selectors for our spinner we will now make them for our attendees reducer. You will end up having a selectors file for each reducer and they can become very logic heavy and need unit test but for now ours are quite simple. 321 | 322 | * Create selector for selecting attendees. 323 | 324 | {% code-tabs %} 325 | {% code-tabs-item title="src/app/event/state/attendees/attendees.selectors.ts" %} 326 | ```typescript 327 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 328 | import { EventState } from '..'; 329 | 330 | export const getEventState = createFeatureSelector('event'); 331 | 332 | export const getAttendeeState = createSelector( 333 | getEventState, 334 | state => state.attendees 335 | ); 336 | 337 | export const getAttendees = createSelector( 338 | getAttendeeState, 339 | state => state.attendees 340 | ); 341 | 342 | 343 | ``` 344 | {% endcode-tabs-item %} 345 | {% endcode-tabs %} 346 | 347 | ## 9. Use selector in EventComponent 348 | 349 | * Use getAttendees selector. 350 | 351 | {% code-tabs %} 352 | {% code-tabs-item title="src/app/event/containers/event/event.component.ts" %} 353 | ```typescript 354 | ---------- ABBREVIATED CODE SNIPPET ---------- 355 | 356 | ngOnInit() { 357 | this.spinner$ = this.store.pipe(select(getSpinner)); 358 | this.attendees$ = this.store.pipe(select(getAttendees)); 359 | this.store.dispatch(new LoadAttendees()); 360 | } 361 | 362 | ---------- ABBREVIATED CODE SNIPPET ---------- 363 | ``` 364 | {% endcode-tabs-item %} 365 | {% endcode-tabs %} 366 | 367 | ## RxJS Operators and higher order observables 368 | 369 | {% hint style="warning" %} 370 | It is important to know which higher order observables to use with your effects to avoid race conditions. A higher order observable is just a fancy name for an observable that emits observable like the ones below. 371 | {% endhint %} 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 388 | 389 | 390 | 391 | 396 | 397 | 398 | 399 | 404 | 405 | 406 | 407 | 412 | 413 | 414 |
OperatorWhen to use
switchMap 384 |

Cancels the current subscription/request and can cause race condition

385 |

Use for get requests or cancelable requests like searches 386 |

387 |
concatMap 392 |

Runs subscriptions/requests in order and is less performant

393 |

Use for get, post and put requests when order is important 394 |

395 |
mergeMap 400 |

Runs subscriptions/requests in parallel

401 |

Use for put, post and delete methods when order is not important 402 |

403 |
exhaustMap 408 |

Ignores all subsequent subscriptions/requests until it completes

409 |

Use for login when you do not want more requests until the initial one is complete 410 |

411 |
## StackBlitz Link 415 | 416 | {% embed data="{\"url\":\"https://stackblitz.com/github/duncanhunter/angular-and-ngrx-demo-app/tree/20-create-effects\",\"type\":\"link\",\"title\":\"StackBlitz\",\"icon\":{\"type\":\"icon\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"thumbnail\":{\"type\":\"thumbnail\",\"url\":\"https://c.staticblitz.com/assets/icon-664493542621427cc8adae5e8f50d632f87aaa6ea1ce5b01e9a3d05b57940a9f.png\",\"aspectRatio\":0},\"caption\":\"Web Link: Link to the demo app running in StackBlitz\"}" %} 417 | 418 | \*\*\*\* 419 | 420 | ## Extras and Homework 421 | 422 | {% hint style="danger" %} 423 | These extra sections are for doing after the course or if you finish a section early. Please move onto the next section if doing this as a workshop when the instructor advises. 424 | 425 | WARNING: Some of these extra sections will make it more difficult to copy and paste the code examples later on in the course. 426 | 427 | You might need to apply the code snippet examples a little more carefully amongst any "extras section" code you may add. If you are up for some extra challenges these sections are for you. 428 | {% endhint %} 429 | 430 | ### Try out the @ngrx/schematics to scaffold out an application. 431 | 432 | Schematics are Angular's way of being able to have custom code generators. You can read more here [https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2](https://blog.angular.io/schematics-an-introduction-dc1dfbc2a2b2) 433 | 434 | You will be wondering why we did not do this from the start and save us a lot of typing? Well first you need to know what you are doing before you can use the scaffolding tools as many students get lost in all the code it makes versus building it up from from nothing for the first time. 435 | 436 | Steps: 437 | 438 | 1. Run this command to install the schematics `npm i @ngrx/schematics -D` 439 | 2. Run this command to set them as the default schematics `ng config cli.defaultCollection @ngrx/schematics` 440 | 3. Run this command to make a default empty root store. `ng g store State --root --statePath state --module app.module.ts` 441 | 4. Run this command to make a container component `ng generate container Event2 --state reducers/index.ts --stateInterface Event2` 442 | 5. Run this command to make the new feature module `ng g module party ng g c party/containers/party --module party/party.module.ts` 443 | 6. Run this command to make the NgRx feature parts like reducer and effects etc `ng generate feature party/party --flat false` 444 | 445 | 446 | 447 | --------------------------------------------------------------------------------