Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

26 testing selectors #12

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d9a0b8f
Step 1: First Actions
toddmotto Nov 26, 2017
179b863
Step 2: Reducer setup
toddmotto Nov 26, 2017
62ae7fb
Step 3: createSelector and createFeatureSelector
toddmotto Nov 26, 2017
8948852
Step 4: NGRX Effects + Action dispatch
toddmotto Nov 27, 2017
f52e5c9
Step 5: Optimising data structures with entities
toddmotto Nov 27, 2017
eb1972d
Step 6: Router Store setup
toddmotto Nov 27, 2017
b2c86b9
Step 7: Custom Router State Serializer
toddmotto Nov 27, 2017
2f697ff
Step 8: Router State and selector composition
toddmotto Nov 27, 2017
ca4e389
Step 9: New Actions for Toppings
toddmotto Nov 28, 2017
52d2a06
Step 10: Toppings Reducer
toddmotto Nov 28, 2017
d9ddd18
Step 11: Toppings Effect
toddmotto Nov 28, 2017
9217fa5
Step 12: Toppings Selectors
toddmotto Nov 28, 2017
60eb3df
Step 13: Selected state IDs
toddmotto Nov 28, 2017
f39259d
Step 14: Visualise Toppings via Dispatches
toddmotto Nov 28, 2017
c3c02d0
Step 15: Creating a Pizza via dispatch, effects, reducers
toddmotto Nov 28, 2017
0fcccca
Step 16: Update pizza and switch fallthrough
toddmotto Nov 29, 2017
c9dc8c3
Step 17: Remove pizza
toddmotto Nov 29, 2017
e2a3a46
Step 18: Router Actions and Effects
toddmotto Nov 29, 2017
6ee65bc
Step 19: Dispatch Routing actions, combine ofType for single effect
toddmotto Nov 29, 2017
be7acbd
Step 20: LoadPizzas Route Guard
toddmotto Nov 29, 2017
b66b89e
Step 21: Pizza exists guard
toddmotto Nov 29, 2017
2e4f28b
Step 22: Toppings Guard
toddmotto Nov 29, 2017
5bcef37
Step 23: Change Detection OnPush
toddmotto Nov 30, 2017
83b23fe
Step 24: Testing Action Creators
toddmotto Nov 30, 2017
d93dd7a
Step 25: Testing Reducers
toddmotto Dec 3, 2017
7558f22
Step 26: Testing Selectors
toddmotto Dec 3, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 9 additions & 47 deletions db.json
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -54,73 +54,35 @@
"name": "Blazin' Inferno",
"toppings": [
{
"id": 10,
"name": "pepperoni"
},
{
"id": 9,
"name": "pepper"
},
{
"id": 3,
"name": "basil"
"id": 12,
"name": "tomato"
},
{
"id": 4,
"name": "chili"
},
{
"id": 7,
"name": "olive"
},
{
"id": 2,
"name": "bacon"
}
],
"id": 1
},
{
"name": "Seaside Surfin'",
"toppings": [
{
"id": 6,
"name": "mushroom"
"id": 10,
"name": "pepperoni"
},
{
"id": 7,
"name": "olive"
"id": 9,
"name": "pepper"
},
{
"id": 2,
"name": "bacon"
"id": 5,
"name": "mozzarella"
},
{
"id": 3,
"name": "basil"
},
{
"id": 1,
"name": "anchovy"
},
{
"id": 8,
"name": "onion"
},
{
"id": 11,
"name": "sweetcorn"
},
{
"id": 9,
"name": "pepper"
},
{
"id": 5,
"name": "mozzarella"
}
],
"id": 2
"id": 1
},
{
"name": "Plain Ol' Pepperoni",
Expand Down
12 changes: 10 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { BrowserModule } from '@angular/platform-browser';
import { Routes, RouterModule } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {
StoreRouterConnectingModule,
RouterStateSerializer,
} from '@ngrx/router-store';
import { StoreModule, MetaReducer } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

import { reducers, effects, CustomSerializer } from './store';

// not used in production
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { storeFreeze } from 'ngrx-store-freeze';
Expand Down Expand Up @@ -37,10 +43,12 @@ export const ROUTES: Routes = [
BrowserModule,
BrowserAnimationsModule,
RouterModule.forRoot(ROUTES),
StoreModule.forRoot({}, { metaReducers }),
EffectsModule.forRoot([]),
StoreModule.forRoot(reducers, { metaReducers }),
EffectsModule.forRoot(effects),
StoreRouterConnectingModule,
environment.development ? StoreDevtoolsModule.instrument() : [],
],
providers: [{ provide: RouterStateSerializer, useClass: CustomSerializer }],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
Expand Down
1 change: 1 addition & 0 deletions src/app/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './router.action';
27 changes: 27 additions & 0 deletions src/app/store/actions/router.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Action } from '@ngrx/store';
import { NavigationExtras } from '@angular/router';

export const GO = '[Router] Go';
export const BACK = '[Router] Back';
export const FORWARD = '[Router] Forward';

export class Go implements Action {
readonly type = GO;
constructor(
public payload: {
path: any[];
query?: object;
extras?: NavigationExtras;
}
) {}
}

export class Back implements Action {
readonly type = BACK;
}

export class Forward implements Action {
readonly type = FORWARD;
}

export type Actions = Go | Back | Forward;
5 changes: 5 additions & 0 deletions src/app/store/effects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RouterEffects } from './router.effect';

export const effects: any[] = [RouterEffects];

export * from './router.effect';
35 changes: 35 additions & 0 deletions src/app/store/effects/router.effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Location } from '@angular/common';

import { Effect, Actions } from '@ngrx/effects';
import * as RouterActions from '../actions/router.action';

import { tap, map } from 'rxjs/operators';

@Injectable()
export class RouterEffects {
constructor(
private actions$: Actions,
private router: Router,
private location: Location
) {}

@Effect({ dispatch: false })
navigate$ = this.actions$.ofType(RouterActions.GO).pipe(
map((action: RouterActions.Go) => action.payload),
tap(({ path, query: queryParams, extras }) => {
this.router.navigate(path, { queryParams, ...extras });
})
);

@Effect({ dispatch: false })
navigateBack$ = this.actions$
.ofType(RouterActions.BACK)
.pipe(tap(() => this.location.back()));

@Effect({ dispatch: false })
navigateForward$ = this.actions$
.ofType(RouterActions.FORWARD)
.pipe(tap(() => this.location.forward()));
}
3 changes: 3 additions & 0 deletions src/app/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './reducers';
export * from './actions';
export * from './effects';
42 changes: 42 additions & 0 deletions src/app/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
ActivatedRouteSnapshot,
RouterStateSnapshot,
Params,
} from '@angular/router';
import { createFeatureSelector, ActionReducerMap } from '@ngrx/store';

import * as fromRouter from '@ngrx/router-store';

export interface RouterStateUrl {
url: string;
queryParams: Params;
params: Params;
}

export interface State {
routerReducer: fromRouter.RouterReducerState<RouterStateUrl>;
}

export const reducers: ActionReducerMap<State> = {
routerReducer: fromRouter.routerReducer,
};

export const getRouterState = createFeatureSelector<
fromRouter.RouterReducerState<RouterStateUrl>
>('routerReducer');

export class CustomSerializer
implements fromRouter.RouterStateSerializer<RouterStateUrl> {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
const { url } = routerState;
const { queryParams } = routerState.root;

let state: ActivatedRouteSnapshot = routerState.root;
while (state.firstChild) {
state = state.firstChild;
}
const { params } = state;

return { url, queryParams, params };
}
}
1 change: 1 addition & 0 deletions src/products/components/pizza-form/pizza-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Topping } from '../../models/topping.model';

@Component({
selector: 'pizza-form',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['pizza-form.component.scss'],
template: `
<div class="pizza-form">
Expand Down
76 changes: 28 additions & 48 deletions src/products/containers/product-item/product-item.component.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,71 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

import { Pizza } from '../../models/pizza.model';
import { PizzasService } from '../../services/pizzas.service';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators';
import * as fromStore from '../../store';

import { Pizza } from '../../models/pizza.model';
import { Topping } from '../../models/topping.model';
import { ToppingsService } from '../../services/toppings.service';

@Component({
selector: 'product-item',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['product-item.component.scss'],
template: `
<div
class="product-item">
<pizza-form
[pizza]="pizza"
[toppings]="toppings"
[pizza]="pizza$ | async"
[toppings]="toppings$ | async"
(selected)="onSelect($event)"
(create)="onCreate($event)"
(update)="onUpdate($event)"
(remove)="onRemove($event)">
<pizza-display
[pizza]="visualise">
[pizza]="visualise$ | async">
</pizza-display>
</pizza-form>
</div>
`,
})
export class ProductItemComponent implements OnInit {
pizza: Pizza;
visualise: Pizza;
toppings: Topping[];
pizza$: Observable<Pizza>;
visualise$: Observable<Pizza>;
toppings$: Observable<Topping[]>;

constructor(
private pizzaService: PizzasService,
private toppingsService: ToppingsService,
private route: ActivatedRoute,
private router: Router
) {}
constructor(private store: Store<fromStore.ProductsState>) {}

ngOnInit() {
this.pizzaService.getPizzas().subscribe(pizzas => {
const param = this.route.snapshot.params.id;
let pizza;
if (param === 'new') {
pizza = {};
} else {
pizza = pizzas.find(pizza => pizza.id == parseInt(param, 10));
}
this.pizza = pizza;
this.toppingsService.getToppings().subscribe(toppings => {
this.toppings = toppings;
this.onSelect(toppings.map(topping => topping.id));
});
});
this.pizza$ = this.store.select(fromStore.getSelectedPizza).pipe(
tap((pizza: Pizza = null) => {
const pizzaExists = !!(pizza && pizza.toppings);
const toppings = pizzaExists
? pizza.toppings.map(topping => topping.id)
: [];
this.store.dispatch(new fromStore.VisualiseToppings(toppings));
})
);
this.toppings$ = this.store.select(fromStore.getAllToppings);
this.visualise$ = this.store.select(fromStore.getPizzaVisualised);
}

onSelect(event: number[]) {
let toppings;
if (this.toppings && this.toppings.length) {
toppings = event.map(id =>
this.toppings.find(topping => topping.id === id)
);
} else {
toppings = this.pizza.toppings;
}
this.visualise = { ...this.pizza, toppings };
this.store.dispatch(new fromStore.VisualiseToppings(event));
}

onCreate(event: Pizza) {
this.pizzaService.createPizza(event).subscribe(pizza => {
this.router.navigate([`/products/${pizza.id}`]);
});
this.store.dispatch(new fromStore.CreatePizza(event));
}

onUpdate(event: Pizza) {
this.pizzaService.updatePizza(event).subscribe(() => {
this.router.navigate([`/products`]);
});
this.store.dispatch(new fromStore.UpdatePizza(event));
}

onRemove(event: Pizza) {
const remove = window.confirm('Are you sure?');
if (remove) {
this.pizzaService.removePizza(event).subscribe(() => {
this.router.navigate([`/products`]);
});
this.store.dispatch(new fromStore.RemovePizza(event));
}
}
}
Loading