Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: CodeSequence/ngconf2019-ngrx-workshop
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: e2426a5f527455283cceb016a96e7b5f4980dcd3
Choose a base ref
..
head repository: CodeSequence/ngconf2019-ngrx-workshop
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 36169996329df736662204075987265e5ed460ff
Choose a head ref
Showing with 468 additions and 386 deletions.
  1. +5 −4 src/app/app.component.css
  2. +9 −5 src/app/app.component.html
  3. +6 −6 src/app/app.component.ts
  4. +5 −5 src/app/app.module.ts
  5. +1 −1 src/app/books/actions/books-api.actions.ts
  6. +3 −3 src/app/books/actions/index.ts
  7. +2 −2 src/app/books/books-api.effects.spec.ts
  8. +34 −31 src/app/books/books-api.effects.ts
  9. +11 −13 src/app/books/books.module.ts
  10. +16 −4 src/app/books/components/book-detail/book-detail.component.html
  11. +4 −4 src/app/books/components/book-detail/book-detail.component.spec.ts
  12. +8 −8 src/app/books/components/book-detail/book-detail.component.ts
  13. +13 −5 src/app/books/components/books-list/books-list.component.html
  14. +4 −4 src/app/books/components/books-list/books-list.component.spec.ts
  15. +5 −5 src/app/books/components/books-list/books-list.component.ts
  16. +8 −7 src/app/books/components/books-page/books-page.component.html
  17. +13 −17 src/app/books/components/books-page/books-page.component.spec.ts
  18. +17 −19 src/app/books/components/books-page/books-page.component.ts
  19. +0 −1 src/app/books/components/books-total/books-total.component.html
  20. +9 −13 src/app/books/components/books-total/books-total.component.spec.ts
  21. +4 −4 src/app/books/components/books-total/books-total.component.ts
  22. +4 −5 src/app/material.module.ts
  23. +16 −4 src/app/movies/components/movie-detail/movie-detail.component.html
  24. +4 −4 src/app/movies/components/movie-detail/movie-detail.component.spec.ts
  25. +8 −8 src/app/movies/components/movie-detail/movie-detail.component.ts
  26. +13 −5 src/app/movies/components/movies-list/movies-list.component.html
  27. +4 −4 src/app/movies/components/movies-list/movies-list.component.spec.ts
  28. +5 −5 src/app/movies/components/movies-list/movies-list.component.ts
  29. +8 −7 src/app/movies/components/movies-page/movies-page.component.html
  30. +43 −40 src/app/movies/components/movies-page/movies-page.component.spec.ts
  31. +15 −13 src/app/movies/components/movies-page/movies-page.component.ts
  32. +0 −1 src/app/movies/components/movies-total/movies-total.component.html
  33. +9 −13 src/app/movies/components/movies-total/movies-total.component.spec.ts
  34. +4 −4 src/app/movies/components/movies-total/movies-total.component.ts
  35. +2 −2 src/app/movies/movie-api.effects.spec.ts
  36. +8 −3 src/app/movies/movie-api.effects.ts
  37. +9 −11 src/app/movies/movies.module.ts
  38. +1 −1 src/app/shared/models/movie.model.ts
  39. +3 −11 src/app/shared/state/__snapshots__/books.reducer.spec.ts.snap
  40. +28 −9 src/app/shared/state/books.reducer.spec.ts
  41. +36 −12 src/app/shared/state/books.reducer.ts
  42. +10 −24 src/app/shared/state/index.ts
  43. +34 −12 src/app/shared/state/movie.reducer.ts
  44. +11 −11 src/index.html
  45. +6 −5 src/main.ts
  46. +1 −2 src/polyfills.ts
  47. +9 −9 src/styles.css
9 changes: 5 additions & 4 deletions src/app/app.component.css
Original file line number Diff line number Diff line change
@@ -5,15 +5,16 @@
}

.nav-link {
color: rgba(0,0,0,.54);
color: rgba(0, 0, 0, 0.54);
display: flex;
align-items:center;
align-items: center;
padding-top: 5px;
padding-bottom: 5px;
}

mat-toolbar {
box-shadow: 0 3px 5px -1px rgba(0,0,0,.2), 0 6px 10px 0 rgba(0,0,0,.14), 0 1px 18px 0 rgba(0,0,0,.12);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
z-index: 1;
}

@@ -22,7 +23,7 @@ mat-toolbar > .mat-mini-fab {
}

mat-sidenav {
box-shadow: 3px 0 6px rgba(0,0,0,.24);
box-shadow: 3px 0 6px rgba(0, 0, 0, 0.24);
width: 200px;
}

14 changes: 9 additions & 5 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<mat-toolbar color="primary">
<button (click)="sidenav.toggle()" mat-mini-fab><mat-icon>menu</mat-icon></button>
<button (click)="sidenav.toggle()" mat-mini-fab>
<mat-icon>menu</mat-icon>
</button>
<span>
{{ title }}
</span>
@@ -8,13 +10,15 @@
<mat-sidenav-container>
<mat-sidenav #sidenav mode="side" class="app-sidenav">
<nav>
<a mat-button
<a
mat-button
class="nav-link"
*ngFor="let link of links"
[routerLink]="link.path"
routerLinkActive="active">
<mat-icon>{{link.icon}}</mat-icon>
{{link.label}}
routerLinkActive="active"
>
<mat-icon>{{ link.icon }}</mat-icon>
{{ link.label }}
</a>
</nav>
</mat-sidenav>
12 changes: 6 additions & 6 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -2,13 +2,13 @@ import { Component } from "@angular/core";

@Component({
selector: "app-root",
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
title = 'NgRx Workshop';
title = "NgRx Workshop";
links = [
{ path: '/movies', icon: 'movie', label: 'Movies'},
{ path: '/books', icon: 'book', label: 'Books'}
];
{ path: "/movies", icon: "movie", label: "Movies" },
{ path: "/books", icon: "book", label: "Books" }
];
}
10 changes: 5 additions & 5 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { NgModule } from "@angular/core";
import { RouterModule } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from "@angular/router";
import { HttpClientModule } from "@angular/common/http";

import { StoreModule } from "@ngrx/store";
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
import { EffectsModule } from "@ngrx/effects";

import { MaterialModule } from './material.module';
import { MaterialModule } from "./material.module";
import { MoviesModule } from "./movies/movies.module";

import { AppComponent } from "./app.component";

import { reducers, metaReducers } from "./shared/state";
import { BooksModule } from './books/books.module';
import { BooksModule } from "./books/books.module";

@NgModule({
declarations: [AppComponent],
@@ -23,7 +23,7 @@ import { BooksModule } from './books/books.module';
BrowserAnimationsModule,
HttpClientModule,
RouterModule.forRoot([
{ path: '', pathMatch: 'full', redirectTo: '/movies' }
{ path: "", pathMatch: "full", redirectTo: "/movies" }
]),
StoreModule.forRoot(reducers, { metaReducers }),
StoreDevtoolsModule.instrument(),
2 changes: 1 addition & 1 deletion src/app/books/actions/books-api.actions.ts
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ export const bookDeleted = createAction(
props<{ book: Book }>()
);

export type BooksApiActions = ReturnType<
export type BooksApiActions = ReturnType<
| typeof booksLoaded
| typeof bookCreated
| typeof bookUpdated
6 changes: 3 additions & 3 deletions src/app/books/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as BooksPageActions from './books-page.actions';
import * as BooksApiActions from './books-api.actions';
import * as BooksPageActions from "./books-page.actions";
import * as BooksApiActions from "./books-api.actions";

export { BooksPageActions, BooksApiActions };
export { BooksPageActions, BooksApiActions };
4 changes: 2 additions & 2 deletions src/app/books/books-api.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ describe("Book API Effects", () => {
const mockBook: Book = {
id: "test",
name: "Mock Book",
earnings: 25,
earnings: 25
};

beforeEach(() => {
@@ -50,7 +50,7 @@ describe("Book API Effects", () => {
const inputAction = BooksPageActions.createBook({
book: {
name: mockBook.name,
earnings: 25,
earnings: 25
}
});
const outputAction = BooksApiActions.bookCreated({
65 changes: 34 additions & 31 deletions src/app/books/books-api.effects.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,66 @@
import { Injectable } from '@angular/core';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { BooksPageActions, BooksApiActions } from './actions';
import { BooksService } from '../shared/services/book.service';
import { mergeMap, map, catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';
import { Injectable } from "@angular/core";
import { Effect, Actions, ofType } from "@ngrx/effects";
import { BooksPageActions, BooksApiActions } from "./actions";
import { BooksService } from "../shared/services/book.service";
import {
mergeMap,
map,
catchError,
exhaustMap,
concatMap
} from "rxjs/operators";
import { EMPTY } from "rxjs";

@Injectable()
export class BooksApiEffects {

@Effect()
loadBooks$ = this.actions$.pipe(
ofType(BooksPageActions.enter.type),
mergeMap(() =>
this.booksService.all()
.pipe(
map(books => BooksApiActions.booksLoaded({books})),
catchError(() => EMPTY)
)
exhaustMap(() =>
this.booksService.all().pipe(
map(books => BooksApiActions.booksLoaded({ books })),
catchError(() => EMPTY)
)
)
);

@Effect()
createBook$ = this.actions$.pipe(
ofType(BooksPageActions.createBook.type),
mergeMap(action =>
this.booksService.create(action.book)
.pipe(
map(book => BooksApiActions.bookCreated({book})),
catchError(() => EMPTY)
)
this.booksService.create(action.book).pipe(
map(book => BooksApiActions.bookCreated({ book })),
catchError(() => EMPTY)
)
)
);

@Effect()
updateBook$ = this.actions$.pipe(
ofType(BooksPageActions.updateBook.type),
mergeMap(action =>
this.booksService.update(action.book.id, action.book)
.pipe(
map(book => BooksApiActions.bookUpdated({book})),
catchError(() => EMPTY)
)
concatMap(action =>
this.booksService.update(action.book.id, action.book).pipe(
map(book => BooksApiActions.bookUpdated({ book })),
catchError(() => EMPTY)
)
)
);

@Effect()
deleteBook$ = this.actions$.pipe(
ofType(BooksPageActions.deleteBook.type),
mergeMap(action =>
this.booksService.delete(action.book.id)
.pipe(
map(() => BooksApiActions.bookDeleted({ book: action.book})),
catchError(() => EMPTY)
)
this.booksService.delete(action.book.id).pipe(
map(() => BooksApiActions.bookDeleted({ book: action.book })),
catchError(() => EMPTY)
)
)
);

constructor(
private booksService: BooksService,
private actions$: Actions<BooksPageActions.BooksActions | BooksApiActions.BooksApiActions>
private actions$: Actions<
BooksPageActions.BooksActions | BooksApiActions.BooksApiActions
>
) {}
}
}
24 changes: 11 additions & 13 deletions src/app/books/books.module.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms";

import { MaterialModule } from 'src/app/material.module';
import { MaterialModule } from "src/app/material.module";

import { BooksPageComponent } from './components/books-page/books-page.component';
import { BookDetailComponent } from './components/book-detail/book-detail.component';
import { BooksListComponent } from './components/books-list/books-list.component';
import { BooksTotalComponent } from './components/books-total/books-total.component';
import { BooksPageComponent } from "./components/books-page/books-page.component";
import { BookDetailComponent } from "./components/book-detail/book-detail.component";
import { BooksListComponent } from "./components/books-list/books-list.component";
import { BooksTotalComponent } from "./components/books-total/books-total.component";

import { EffectsModule } from '@ngrx/effects';
import { BooksApiEffects } from './books-api.effects';
import { EffectsModule } from "@ngrx/effects";
import { BooksApiEffects } from "./books-api.effects";

@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
MaterialModule,
RouterModule.forChild([
{ path: 'books', component: BooksPageComponent }
]),
RouterModule.forChild([{ path: "books", component: BooksPageComponent }]),
EffectsModule.forFeature([BooksApiEffects])
],
declarations: [
20 changes: 16 additions & 4 deletions src/app/books/components/book-detail/book-detail.component.html
Original file line number Diff line number Diff line change
@@ -2,21 +2,33 @@
<mat-card-header>
<mat-card-title>
<h1>
<span *ngIf="originalBook?.id; else prompt">Editing {{originalBook.name}}</span>
<span *ngIf="originalBook?.id; else prompt"
>Editing {{ originalBook.name }}</span
>
<ng-template #prompt>Create Book</ng-template>
</h1>
</mat-card-title>
</mat-card-header>
<form [formGroup]="bookForm" #f="ngForm" (submit)="onSubmit(f.value)">
<mat-card-content>
<mat-form-field class="full-width">
<input matInput placeholder="Name" formControlName="name" type="text">
<input matInput placeholder="Name" formControlName="name" type="text" />
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Earnings" formControlName="earnings" type="text">
<input
matInput
placeholder="Earnings"
formControlName="earnings"
type="text"
/>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Description" formControlName="description" type="text">
<input
matInput
placeholder="Description"
formControlName="description"
type="text"
/>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import { BookDetailComponent } from './book-detail.component';
import { TestBed, async } from "@angular/core/testing";
import { BookDetailComponent } from "./book-detail.component";

describe('Component: BookDetail', () => {
it('should create an instance', () => {
describe("Component: BookDetail", () => {
it("should create an instance", () => {
const component = new BookDetailComponent();
expect(component).toBeTruthy();
});
16 changes: 8 additions & 8 deletions src/app/books/components/book-detail/book-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Book } from 'src/app/shared/models/book.model';
import { FormGroup, FormControl } from '@angular/forms';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Book } from "src/app/shared/models/book.model";
import { FormGroup, FormControl } from "@angular/forms";

@Component({
selector: 'app-book-detail',
templateUrl: './book-detail.component.html',
styleUrls: ['./book-detail.component.css']
selector: "app-book-detail",
templateUrl: "./book-detail.component.html",
styleUrls: ["./book-detail.component.css"]
})
export class BookDetailComponent {
originalBook: Book | undefined;
@Output() save = new EventEmitter();
@Output() cancel = new EventEmitter();

bookForm = new FormGroup({
name: new FormControl(''),
name: new FormControl(""),
earnings: new FormControl(0),
description: new FormControl('')
description: new FormControl("")
});

@Input() set book(book: Book) {
18 changes: 13 additions & 5 deletions src/app/books/components/books-list/books-list.component.html
Original file line number Diff line number Diff line change
@@ -6,15 +6,23 @@ <h1>Books</h1>
</mat-card-header>
<mat-card-content>
<mat-list>
<mat-list-item *ngFor="let book of books" (click)="select.emit(book)" class="record">
<h3 mat-line>{{book.name}}</h3>
<mat-list-item
*ngFor="let book of books"
(click)="select.emit(book)"
class="record"
>
<h3 mat-line>{{ book.name }}</h3>
<p mat-line>
{{book.description}}
{{ book.description }}
</p>
<p>
{{book.earnings | currency}}
{{ book.earnings | currency }}
</p>
<button *ngIf="!readonly" mat-icon-button (click)="delete.emit(book);$event.stopImmediatePropagation();">
<button
*ngIf="!readonly"
mat-icon-button
(click)="delete.emit(book); $event.stopImmediatePropagation()"
>
<mat-icon>close</mat-icon>
</button>
</mat-list-item>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import { BooksListComponent } from './books-list.component';
import { TestBed, async } from "@angular/core/testing";
import { BooksListComponent } from "./books-list.component";

describe('Component: BooksList', () => {
it('should create an instance', () => {
describe("Component: BooksList", () => {
it("should create an instance", () => {
const component = new BooksListComponent();
expect(component).toBeTruthy();
});
10 changes: 5 additions & 5 deletions src/app/books/components/books-list/books-list.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Book } from 'src/app/shared/models/book.model';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Book } from "src/app/shared/models/book.model";

@Component({
selector: 'app-books-list',
templateUrl: './books-list.component.html',
styleUrls: ['./books-list.component.css']
selector: "app-books-list",
templateUrl: "./books-list.component.html",
styleUrls: ["./books-list.component.css"]
})
export class BooksListComponent {
@Input() books: Book[];
15 changes: 8 additions & 7 deletions src/app/books/components/books-page/books-page.component.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<div class="container">
<div class="col-50">
<app-books-total
[total]="total$ | async">
</app-books-total>
<app-books-total [total]="total$ | async"> </app-books-total>

<app-books-list
[books]="books$ | async"
(select)="onSelect($event)"
(delete)="onDelete($event)">
(delete)="onDelete($event)"
>
</app-books-list>
</div>

<app-book-detail class="col-50"
[book]="currentBook$ | async"
<app-book-detail
class="col-50"
[book]="activeBook$ | async"
(save)="onSave($event)"
(cancel)="onCancel()">
(cancel)="onCancel()"
>
</app-book-detail>
</div>
30 changes: 13 additions & 17 deletions src/app/books/components/books-page/books-page.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import { TestBed, async, ComponentFixture } from "@angular/core/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { ReactiveFormsModule } from "@angular/forms";

import { MaterialModule } from 'src/app/material.module';
import { MaterialModule } from "src/app/material.module";

import { BooksPageComponent } from './books-page.component';
import { BooksService } from 'src/app/shared/services/book.service';
import { BooksListComponent } from '../books-list/books-list.component';
import { BookDetailComponent } from '../book-detail/book-detail.component';
import { BooksTotalComponent } from '../books-total/books-total.component';
import { provideMockStore } from '@ngrx/store/testing';
import { BooksPageComponent } from "./books-page.component";
import { BooksService } from "src/app/shared/services/book.service";
import { BooksListComponent } from "../books-list/books-list.component";
import { BookDetailComponent } from "../book-detail/book-detail.component";
import { BooksTotalComponent } from "../books-total/books-total.component";
import { provideMockStore } from "@ngrx/store/testing";

class BooksServiceStub {}

describe('Component: Books Page', () => {
describe("Component: Books Page", () => {
let comp: BooksPageComponent;
let fixture: ComponentFixture<BooksPageComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
NoopAnimationsModule,
ReactiveFormsModule
],
imports: [MaterialModule, NoopAnimationsModule, ReactiveFormsModule],
declarations: [
BooksPageComponent,
BooksPageComponent,
@@ -43,7 +39,7 @@ describe('Component: Books Page', () => {
comp = fixture.componentInstance;
});

it('should create an instance', () => {
it("should create an instance", () => {
expect(comp).toBeTruthy();
});
});
36 changes: 17 additions & 19 deletions src/app/books/components/books-page/books-page.component.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit } from "@angular/core";

import { Book } from 'src/app/shared/models/book.model';
import { Book } from "src/app/shared/models/book.model";

import { Observable } from 'rxjs';
import { Store, select } from '@ngrx/store';
import * as fromRoot from 'src/app/shared/state';
import { BooksPageActions } from '../../actions';
import { Observable } from "rxjs";
import { Store, select } from "@ngrx/store";
import * as fromRoot from "src/app/shared/state";
import { BooksPageActions } from "../../actions";

@Component({
selector: 'app-books',
templateUrl: './books-page.component.html',
styleUrls: ['./books-page.component.css']
selector: "app-books",
templateUrl: "./books-page.component.html",
styleUrls: ["./books-page.component.css"]
})
export class BooksPageComponent implements OnInit {
books$: Observable<Book[]>;
currentBook$: Observable<Book>;
activeBook$: Observable<Book>;
total$: Observable<number>;

constructor(
private store: Store<fromRoot.State>
) {
constructor(private store: Store<fromRoot.State>) {
this.books$ = this.store.pipe(select(fromRoot.selectAllBooks));
this.currentBook$ = this.store.pipe(select(fromRoot.selectActiveBook));
this.activeBook$ = this.store.pipe(select(fromRoot.selectActiveBook));
this.total$ = this.store.pipe(select(fromRoot.selectBookEarningsTotals));
}

@@ -35,7 +33,7 @@ export class BooksPageComponent implements OnInit {
}

onSelect(book: Book) {
this.store.dispatch(BooksPageActions.selectBook({bookId: book.id}));
this.store.dispatch(BooksPageActions.selectBook({ bookId: book.id }));
}

onCancel() {
@@ -44,7 +42,7 @@ export class BooksPageComponent implements OnInit {

removeSelectedBook() {
this.store.dispatch(BooksPageActions.clearSelectedBook());
}
}

onSave(book: Book) {
if (!book.id) {
@@ -55,14 +53,14 @@ export class BooksPageComponent implements OnInit {
}

saveBook(book: Book) {
this.store.dispatch(BooksPageActions.createBook({book}));
this.store.dispatch(BooksPageActions.createBook({ book }));
}

updateBook(book: Book) {
this.store.dispatch(BooksPageActions.updateBook({book, changes: book}));
this.store.dispatch(BooksPageActions.updateBook({ book, changes: book }));
}

onDelete(book: Book) {
this.store.dispatch(BooksPageActions.deleteBook({book}));
this.store.dispatch(BooksPageActions.deleteBook({ book }));
}
}
Original file line number Diff line number Diff line change
@@ -8,4 +8,3 @@ <h1>Books Gross Total</h1>
{{ total | currency }}
</mat-card-content>
</mat-card>

22 changes: 9 additions & 13 deletions src/app/books/components/books-total/books-total.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from "@angular/core/testing";

import { BooksTotalComponent } from './books-total.component';
import { MaterialModule } from 'src/app/material.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { BooksTotalComponent } from "./books-total.component";
import { MaterialModule } from "src/app/material.module";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";

describe('BooksTotalComponent', () => {
describe("BooksTotalComponent", () => {
let component: BooksTotalComponent;
let fixture: ComponentFixture<BooksTotalComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
NoopAnimationsModule
],
declarations: [ BooksTotalComponent ]
})
.compileComponents();
imports: [MaterialModule, NoopAnimationsModule],
declarations: [BooksTotalComponent]
}).compileComponents();
}));

beforeEach(() => {
@@ -25,7 +21,7 @@ describe('BooksTotalComponent', () => {
fixture.detectChanges();
});

it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});
8 changes: 4 additions & 4 deletions src/app/books/components/books-total/books-total.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, Input } from '@angular/core';
import { Component, Input } from "@angular/core";

@Component({
selector: 'app-books-total',
templateUrl: './books-total.component.html',
styleUrls: ['./books-total.component.css']
selector: "app-books-total",
templateUrl: "./books-total.component.html",
styleUrls: ["./books-total.component.css"]
})
export class BooksTotalComponent {
@Input() total: number;
9 changes: 4 additions & 5 deletions src/app/material.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NgModule } from '@angular/core';
import { NgModule } from "@angular/core";
import {
MatButtonModule,
MatCardModule,
@@ -7,8 +7,8 @@ import {
MatInputModule,
MatListModule,
MatSidenavModule,
MatToolbarModule,
} from '@angular/material';
MatToolbarModule
} from "@angular/material";

@NgModule({
imports: [
@@ -32,5 +32,4 @@ import {
MatToolbarModule
]
})
export class MaterialModule {
}
export class MaterialModule {}
20 changes: 16 additions & 4 deletions src/app/movies/components/movie-detail/movie-detail.component.html
Original file line number Diff line number Diff line change
@@ -2,21 +2,33 @@
<mat-card-header>
<mat-card-title>
<h1>
<span *ngIf="originalMovie?.id; else prompt">Editing {{originalMovie.name}}</span>
<span *ngIf="originalMovie?.id; else prompt"
>Editing {{ originalMovie.name }}</span
>
<ng-template #prompt>Create Movie</ng-template>
</h1>
</mat-card-title>
</mat-card-header>
<form [formGroup]="movieForm" #f="ngForm" (submit)="onSubmit(f.value)">
<mat-card-content>
<mat-form-field class="full-width">
<input matInput placeholder="Name" formControlName="name" type="text">
<input matInput placeholder="Name" formControlName="name" type="text" />
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Earnings" formControlName="earnings" type="text">
<input
matInput
placeholder="Earnings"
formControlName="earnings"
type="text"
/>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Description" formControlName="description" type="text">
<input
matInput
placeholder="Description"
formControlName="description"
type="text"
/>
</mat-form-field>
</mat-card-content>
<mat-card-actions>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import { MovieDetailComponent } from './movie-detail.component';
import { TestBed, async } from "@angular/core/testing";
import { MovieDetailComponent } from "./movie-detail.component";

describe('Component: MovieDetail', () => {
it('should create an instance', () => {
describe("Component: MovieDetail", () => {
it("should create an instance", () => {
const component = new MovieDetailComponent();
expect(component).toBeTruthy();
});
16 changes: 8 additions & 8 deletions src/app/movies/components/movie-detail/movie-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Movie } from 'src/app/shared/models/movie.model';
import { FormGroup, FormControl } from '@angular/forms';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Movie } from "src/app/shared/models/movie.model";
import { FormGroup, FormControl } from "@angular/forms";

@Component({
selector: 'app-movie-detail',
templateUrl: './movie-detail.component.html',
styleUrls: ['./movie-detail.component.css']
selector: "app-movie-detail",
templateUrl: "./movie-detail.component.html",
styleUrls: ["./movie-detail.component.css"]
})
export class MovieDetailComponent {
originalMovie: Movie | undefined;
@Output() save = new EventEmitter();
@Output() cancel = new EventEmitter();

movieForm = new FormGroup({
name: new FormControl(''),
name: new FormControl(""),
earnings: new FormControl(0),
description: new FormControl('')
description: new FormControl("")
});

@Input() set movie(movie: Movie) {
18 changes: 13 additions & 5 deletions src/app/movies/components/movies-list/movies-list.component.html
Original file line number Diff line number Diff line change
@@ -6,15 +6,23 @@ <h1>Movies</h1>
</mat-card-header>
<mat-card-content>
<mat-list>
<mat-list-item *ngFor="let movie of movies" (click)="select.emit(movie)" class="record">
<h3 mat-line>{{movie.name}}</h3>
<mat-list-item
*ngFor="let movie of movies"
(click)="select.emit(movie)"
class="record"
>
<h3 mat-line>{{ movie.name }}</h3>
<p mat-line>
{{movie.description}}
{{ movie.description }}
</p>
<p>
{{movie.earnings | currency}}
{{ movie.earnings | currency }}
</p>
<button *ngIf="!readonly" mat-icon-button (click)="delete.emit(movie);$event.stopImmediatePropagation();">
<button
*ngIf="!readonly"
mat-icon-button
(click)="delete.emit(movie); $event.stopImmediatePropagation()"
>
<mat-icon>close</mat-icon>
</button>
</mat-list-item>
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* tslint:disable:no-unused-variable */

import { TestBed, async } from '@angular/core/testing';
import { MoviesListComponent } from './movies-list.component';
import { TestBed, async } from "@angular/core/testing";
import { MoviesListComponent } from "./movies-list.component";

describe('Component: MoviesList', () => {
it('should create an instance', () => {
describe("Component: MoviesList", () => {
it("should create an instance", () => {
const component = new MoviesListComponent();
expect(component).toBeTruthy();
});
10 changes: 5 additions & 5 deletions src/app/movies/components/movies-list/movies-list.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Movie } from 'src/app/shared/models/movie.model';
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Movie } from "src/app/shared/models/movie.model";

@Component({
selector: 'app-movies-list',
templateUrl: './movies-list.component.html',
styleUrls: ['./movies-list.component.css']
selector: "app-movies-list",
templateUrl: "./movies-list.component.html",
styleUrls: ["./movies-list.component.css"]
})
export class MoviesListComponent {
@Input() movies: Movie[];
15 changes: 8 additions & 7 deletions src/app/movies/components/movies-page/movies-page.component.html
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
<div class="container">
<div class="col-50">
<app-movies-total
[total]="total$ | async">
</app-movies-total>
<app-movies-total [total]="total$ | async"> </app-movies-total>

<app-movies-list
[movies]="movies$ | async"
(select)="onSelect($event)"
(delete)="onDelete($event)">
(delete)="onDelete($event)"
>
</app-movies-list>
</div>

<app-movie-detail class="col-50"
[movie]="currentMovie$ | async"
<app-movie-detail
class="col-50"
[movie]="activeMovie$ | async"
(save)="onSave($event)"
(cancel)="onCancel()">
(cancel)="onCancel()"
>
</app-movie-detail>
</div>
83 changes: 43 additions & 40 deletions src/app/movies/components/movies-page/movies-page.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,106 @@
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { By } from "@angular/platform-browser";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { TestBed, ComponentFixture } from "@angular/core/testing";
import { provideMockStore, MockStore } from "@ngrx/store/testing";
import { ReactiveFormsModule } from "@angular/forms";
import { Store } from "@ngrx/store";

import { MaterialModule } from 'src/app/material.module';
import { MaterialModule } from "src/app/material.module";

import { MoviesPageActions, MovieApiActions } from '../../actions';
import * as fromRoot from 'src/app/shared/state';
import { Movie } from 'src/app/shared/models/movie.model';
import { MoviesPageActions } from "../../actions";
import { Movie } from "src/app/shared/models/movie.model";

import { MoviesPageComponent } from './movies-page.component';
import { MoviesListComponent } from '../movies-list/movies-list.component';
import { MovieDetailComponent } from '../movie-detail/movie-detail.component';
import { MoviesTotalComponent } from '../movies-total/movies-total.component';
import { MoviesPageComponent } from "./movies-page.component";
import { MoviesListComponent } from "../movies-list/movies-list.component";
import { MovieDetailComponent } from "../movie-detail/movie-detail.component";
import { MoviesTotalComponent } from "../movies-total/movies-total.component";

describe('Component: Movies Page', () => {
describe("Component: Movies Page", () => {
let comp: MoviesPageComponent;
let fixture: ComponentFixture<MoviesPageComponent>;
let store: MockStore<{ movies: Movie[] }>;

beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
NoopAnimationsModule,
ReactiveFormsModule
],
imports: [MaterialModule, NoopAnimationsModule, ReactiveFormsModule],
declarations: [
MoviesPageComponent,
MoviesListComponent,
MovieDetailComponent,
MoviesTotalComponent
],
providers: [
provideMockStore()
]
providers: [provideMockStore()]
});

fixture = TestBed.createComponent(MoviesPageComponent);
comp = fixture.componentInstance;
store = TestBed.get(Store);

spyOn(store, 'dispatch').and.callThrough();
spyOn(store, "dispatch").and.callThrough();
});

it('should create an instance', () => {
it("should create an instance", () => {
expect(comp).toBeTruthy();
});

it('should display an Enter action on init', () => {
it("should display an Enter action on init", () => {
const action = MoviesPageActions.enter();

comp.ngOnInit();

expect(store.dispatch).toHaveBeenCalledWith(action);
});

it('should dispatch an select action on a select event from the movie list', () => {
const movie: Movie = { id: "1", name: 'Movie', earnings: 25 };
it("should dispatch an select action on a select event from the movie list", () => {
const movie: Movie = { id: "1", name: "Movie", earnings: 25 };
const action = MoviesPageActions.selectMovie({ movieId: movie.id });

fixture.debugElement.query(By.css('app-movies-list')).triggerEventHandler('select', movie);
fixture.debugElement
.query(By.css("app-movies-list"))
.triggerEventHandler("select", movie);

expect(store.dispatch).toHaveBeenCalledWith(action);
});

it('should dispatch an delete action on a delete event from the movie list', () => {
const movie: Movie = { id: "1", name: 'Movie', earnings: 25 };
it("should dispatch an delete action on a delete event from the movie list", () => {
const movie: Movie = { id: "1", name: "Movie", earnings: 25 };
const action = MoviesPageActions.deleteMovie({ movie });

fixture.debugElement.query(By.css('app-movies-list')).triggerEventHandler('delete', movie);
fixture.debugElement
.query(By.css("app-movies-list"))
.triggerEventHandler("delete", movie);

expect(store.dispatch).toHaveBeenCalledWith(action);
});

it('should dispatch an save action on a save event from the movie details', () => {
const movie: Movie = { id: undefined, name: 'Movie', earnings: 25 };
it("should dispatch an save action on a save event from the movie details", () => {
const movie: Movie = { id: undefined, name: "Movie", earnings: 25 };
const action = MoviesPageActions.createMovie({ movie });

fixture.debugElement.query(By.css('app-movie-detail')).triggerEventHandler('save', movie);
fixture.debugElement
.query(By.css("app-movie-detail"))
.triggerEventHandler("save", movie);

expect(store.dispatch).toHaveBeenCalledWith(action);
});

it('should dispatch an update action on a delete event from the movie details', () => {
const movie: Movie = { id: "1", name: 'Movie', earnings: 25 };
it("should dispatch an update action on a delete event from the movie details", () => {
const movie: Movie = { id: "1", name: "Movie", earnings: 25 };
const action = MoviesPageActions.updateMovie({ movie, changes: movie });

fixture.debugElement.query(By.css('app-movie-detail')).triggerEventHandler('save', movie);
fixture.debugElement
.query(By.css("app-movie-detail"))
.triggerEventHandler("save", movie);

expect(store.dispatch).toHaveBeenCalledWith(action);
});

it('should dispatch an clear action on a cancel event from the movie details', () => {
it("should dispatch an clear action on a cancel event from the movie details", () => {
const action = MoviesPageActions.clearSelectedMovie();

fixture.debugElement.query(By.css('app-movie-detail')).triggerEventHandler('cancel', null);
fixture.debugElement
.query(By.css("app-movie-detail"))
.triggerEventHandler("cancel", null);

expect(store.dispatch).toHaveBeenCalledWith(action);
});
28 changes: 15 additions & 13 deletions src/app/movies/components/movies-page/movies-page.component.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Component, OnInit } from "@angular/core";
import { Store, select } from "@ngrx/store";

import { MoviesPageActions } from '../../actions';
import { Movie } from 'src/app/shared/models/movie.model';
import * as fromRoot from 'src/app/shared/state';
import { MoviesPageActions } from "../../actions";
import { Movie } from "src/app/shared/models/movie.model";
import * as fromRoot from "src/app/shared/state";

@Component({
selector: 'app-movies',
templateUrl: './movies-page.component.html',
styleUrls: ['./movies-page.component.css']
selector: "app-movies",
templateUrl: "./movies-page.component.html",
styleUrls: ["./movies-page.component.css"]
})
export class MoviesPageComponent implements OnInit {
movies$ = this.store.pipe(select(fromRoot.selectMovies));
currentMovie$ = this.store.pipe(select(fromRoot.selectCurrentMovie));
activeMovie$ = this.store.pipe(select(fromRoot.selectActiveMovie));
total$ = this.store.pipe(select(fromRoot.selectMoviesEarningsTotal));

constructor(private store: Store<fromRoot.State>) {}
@@ -22,7 +22,7 @@ export class MoviesPageComponent implements OnInit {
}

onSelect(movie: Movie) {
this.store.dispatch(MoviesPageActions.selectMovie({movieId: movie.id}));
this.store.dispatch(MoviesPageActions.selectMovie({ movieId: movie.id }));
}

onCancel() {
@@ -38,14 +38,16 @@ export class MoviesPageComponent implements OnInit {
}

saveMovie(movie: Movie) {
this.store.dispatch(MoviesPageActions.createMovie({movie}));
this.store.dispatch(MoviesPageActions.createMovie({ movie }));
}

updateMovie(movie: Movie) {
this.store.dispatch(MoviesPageActions.updateMovie({movie, changes: movie}));
this.store.dispatch(
MoviesPageActions.updateMovie({ movie, changes: movie })
);
}

onDelete(movie: Movie) {
this.store.dispatch(MoviesPageActions.deleteMovie({movie}));
this.store.dispatch(MoviesPageActions.deleteMovie({ movie }));
}
}
Original file line number Diff line number Diff line change
@@ -8,4 +8,3 @@ <h1>Movies Gross Total</h1>
{{ total | currency }}
</mat-card-content>
</mat-card>

Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from "@angular/core/testing";

import { MoviesTotalComponent } from './movies-total.component';
import { MaterialModule } from 'src/app/material.module';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MoviesTotalComponent } from "./movies-total.component";
import { MaterialModule } from "src/app/material.module";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";

describe('MoviesTotalComponent', () => {
describe("MoviesTotalComponent", () => {
let component: MoviesTotalComponent;
let fixture: ComponentFixture<MoviesTotalComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MaterialModule,
NoopAnimationsModule
],
declarations: [ MoviesTotalComponent ]
})
.compileComponents();
imports: [MaterialModule, NoopAnimationsModule],
declarations: [MoviesTotalComponent]
}).compileComponents();
}));

beforeEach(() => {
@@ -25,7 +21,7 @@ describe('MoviesTotalComponent', () => {
fixture.detectChanges();
});

it('should create', () => {
it("should create", () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, Input } from '@angular/core';
import { Component, Input } from "@angular/core";

@Component({
selector: 'app-movies-total',
templateUrl: './movies-total.component.html',
styleUrls: ['./movies-total.component.css']
selector: "app-movies-total",
templateUrl: "./movies-total.component.html",
styleUrls: ["./movies-total.component.css"]
})
export class MoviesTotalComponent {
@Input() total: number;
4 changes: 2 additions & 2 deletions src/app/movies/movie-api.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ describe("Movie API Effects", () => {
const mockMovie: Movie = {
id: "test",
name: "Mock Movie",
earnings: 25,
earnings: 25
};

beforeEach(() => {
@@ -50,7 +50,7 @@ describe("Movie API Effects", () => {
const inputAction = MoviesPageActions.createMovie({
movie: {
name: mockMovie.name,
earnings: 25,
earnings: 25
}
});
const outputAction = MovieApiActions.createMovieSuccess({
11 changes: 8 additions & 3 deletions src/app/movies/movie-api.effects.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Injectable } from "@angular/core";
import { Effect, Actions, ofType } from "@ngrx/effects";
import { of } from "rxjs";
import { mergeMap, map, catchError, concatMap } from "rxjs/operators";
import {
mergeMap,
map,
catchError,
concatMap,
exhaustMap
} from "rxjs/operators";
import { MovieApiActions, MoviesPageActions } from "./actions";
import { MoviesService } from "../shared/services/movies.service";

@Injectable()
export class MovieApiEffects {

constructor(
private actions$: Actions<MoviesPageActions.Union>,
private movieService: MoviesService
) {}

@Effect() enterMoviesPage$ = this.actions$.pipe(
ofType(MoviesPageActions.enter.type),
mergeMap(() =>
exhaustMap(() =>
this.movieService.all().pipe(
map(movies => MovieApiActions.loadMoviesSuccess({ movies })),
catchError(() => of(MovieApiActions.loadMoviesFailure()))
20 changes: 9 additions & 11 deletions src/app/movies/movies.module.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { NgModule } from "@angular/core";
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { ReactiveFormsModule } from "@angular/forms";

import { EffectsModule } from "@ngrx/effects";

import { MaterialModule } from 'src/app/material.module';
import { MaterialModule } from "src/app/material.module";
import { MovieApiEffects } from "./movie-api.effects";

import { MoviesPageComponent } from './components/movies-page/movies-page.component';
import { MovieDetailComponent } from './components/movie-detail/movie-detail.component';
import { MoviesListComponent } from './components/movies-list/movies-list.component';
import { MoviesTotalComponent } from './components/movies-total/movies-total.component';
import { MoviesPageComponent } from "./components/movies-page/movies-page.component";
import { MovieDetailComponent } from "./components/movie-detail/movie-detail.component";
import { MoviesListComponent } from "./components/movies-list/movies-list.component";
import { MoviesTotalComponent } from "./components/movies-total/movies-total.component";

@NgModule({
imports: [
CommonModule,
ReactiveFormsModule,
MaterialModule,
RouterModule.forChild([
{ path: 'movies', component: MoviesPageComponent }
]),
RouterModule.forChild([{ path: "movies", component: MoviesPageComponent }]),
EffectsModule.forFeature([MovieApiEffects])
],
declarations: [
2 changes: 1 addition & 1 deletion src/app/shared/models/movie.model.ts
Original file line number Diff line number Diff line change
@@ -5,4 +5,4 @@ export interface Movie {
description?: string;
}

export type MovieRequiredProps = Pick<Movie, "name" | "earnings">;
export type MovieRequiredProps = Pick<Movie, "name" | "earnings">;
14 changes: 3 additions & 11 deletions src/app/shared/state/__snapshots__/books.reducer.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -34,16 +34,8 @@ Object {

exports[`Books Reducer should remove books from the state when they are deleted 1`] = `
Object {
"activeBookId": "1",
"entities": Object {
"1": Object {
"earnings": 1000,
"id": "1",
"name": "Apollo 13",
},
},
"ids": Array [
"1",
],
"activeBookId": null,
"entities": Object {},
"ids": Array [],
}
`;
37 changes: 28 additions & 9 deletions src/app/shared/state/books.reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { BooksApiActions, BooksPageActions } from "src/app/books/actions";
import { Book } from "../models/book.model";
import { reducer, initialState, activeBookId, adapter, selectAll } from "./books.reducer";
import {
reducer,
initialState,
selectActiveBook,
adapter,
selectAll
} from "./books.reducer";

describe("Books Reducer", () => {
it("should return the initial state when initialized", () => {
@@ -30,32 +36,45 @@ describe("Books Reducer", () => {
it("should remove books from the state when they are deleted", () => {
const book: Book = { id: "1", name: "Apollo 13", earnings: 1000 };
const firstAction = BooksApiActions.bookCreated({ book });
const secondAction = BooksPageActions.deleteBook({ book });
const secondAction = BooksApiActions.bookDeleted({ book });

const state = [firstAction, secondAction].reduce(reducer, initialState);

expect(state).toMatchSnapshot();
});

describe("Selectors", () => {
const initialState = { activeBookId: "1", ids: [], entities: {} };
const initialState = { activeBookId: null, ids: [], entities: {} };

describe("activeBookId", () => {
it("should return the active book id", () => {
const result = activeBookId(initialState);
describe("selectActiveBook", () => {
it("should return null if there is no active book", () => {
const result = selectActiveBook(initialState);

expect(result).toBe("1");
expect(result).toBe(null);
});

it("should return the active book if there is one", () => {
const book: Book = { id: "1", name: "Castaway", earnings: 1000000 };
const state = adapter.addAll([book], {
...initialState,
activeBookId: "1"
});
const result = selectActiveBook(state);

expect(result).toBe(book);
});
});

describe("selectAll", () => {
it("should return all the loaded books", () => {
const books: Book[] = [{ id: "1", name: "Castaway", earnings: 1000000 }];
const books: Book[] = [
{ id: "1", name: "Castaway", earnings: 1000000 }
];
const state = adapter.addAll(books, initialState);
const result = selectAll(state);

expect(result.length).toBe(1);
});
});
});
});
});
48 changes: 36 additions & 12 deletions src/app/shared/state/books.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { Book } from 'src/app/shared/models/book.model';
import { BooksPageActions, BooksApiActions } from 'src/app/books/actions';

import { createEntityAdapter, EntityAdapter, EntityState } from "@ngrx/entity";
import { Book } from "src/app/shared/models/book.model";
import { BooksPageActions, BooksApiActions } from "src/app/books/actions";
import { createSelector } from "@ngrx/store";

export const initialBooks: Book[] = [
{
@@ -21,7 +21,7 @@ export const initialBooks: Book[] = [
name: "The Return of The King",
earnings: 400000000,
description: "The end"
},
}
];

export interface State extends EntityState<Book> {
@@ -34,11 +34,14 @@ export const initialState = adapter.getInitialState({
activeBookId: null
});

export function reducer(state = initialState, action: BooksPageActions.BooksActions | BooksApiActions.BooksApiActions): State {
switch(action.type) {
export function reducer(
state = initialState,
action: BooksPageActions.BooksActions | BooksApiActions.BooksApiActions
): State {
switch (action.type) {
case BooksApiActions.booksLoaded.type:
return adapter.addAll(action.books, state);

case BooksPageActions.selectBook.type:
return {
...state,
@@ -52,18 +55,39 @@ export function reducer(state = initialState, action: BooksPageActions.BooksActi
};

case BooksApiActions.bookCreated.type:
return adapter.addOne(action.book, {...state, activeBookId: action.book.id});
return adapter.addOne(action.book, {
...state,
activeBookId: action.book.id
});

case BooksApiActions.bookUpdated.type:
return adapter.updateOne({id: action.book.id, changes: action.book}, {...state, activeBookId: action.book.id});
return adapter.updateOne(
{ id: action.book.id, changes: action.book },
{ ...state, activeBookId: action.book.id }
);

case BooksApiActions.bookDeleted.type:
return adapter.removeOne(action.book.id, {...state, activeBookId: null});
return adapter.removeOne(action.book.id, {
...state,
activeBookId: null
});

default:
return state;
}
}

export const { selectAll, selectEntities } = adapter.getSelectors();
export const activeBookId = (state: State) => state.activeBookId;
export const selectActiveBookId = (state: State) => state.activeBookId;
export const selectActiveBook = createSelector(
selectEntities,
selectActiveBookId,
(entities, bookId) => (bookId ? entities[bookId] : null)
);
export const selectEarningsTotals = createSelector(
selectAll,
books =>
books.reduce((total, book) => {
return total + parseInt(`${book.earnings}`, 10) || 0;
}, 0)
);
34 changes: 10 additions & 24 deletions src/app/shared/state/index.ts
Original file line number Diff line number Diff line change
@@ -34,43 +34,29 @@ export const selectActiveMovieId = createSelector(
fromMovies.selectActiveMovieId
);

export const selectCurrentMovie = createSelector(
selectMovieEntities,
selectActiveMovieId,
(movies, activeMovieId) => activeMovieId && movies[activeMovieId]
export const selectActiveMovie = createSelector(
selectMovieState,
fromMovies.selectActiveMovie
);

export const selectMoviesEarningsTotal = createSelector(
selectMovies,
movies => movies.reduce((total, movie) => total + parseInt(`${movie.earnings}`, 10) || 0, 0)
selectMovieState,
fromMovies.selectEarningsTotal
);

export const selectBooksState = (state: State) => state.books;

export const selectActiveBookId = createSelector(
selectBooksState,
fromBooks.activeBookId
);

export const selectAllBooks = createSelector(
selectBooksState,
fromBooks.selectAll
);

export const selectAllBooksEntities = createSelector(
selectBooksState,
fromBooks.selectEntities
);

export const selectActiveBook = createSelector(
selectAllBooksEntities,
selectActiveBookId,
(entities, bookId) => bookId ? entities[bookId] : null
selectBooksState,
fromBooks.selectActiveBook
);

export const selectBookEarningsTotals = createSelector(
selectAllBooks,
books => books.reduce((total, book) => {
return total + parseInt(`${book.earnings}`, 10) || 0;
}, 0)
)
selectBooksState,
fromBooks.selectEarningsTotals
);
46 changes: 34 additions & 12 deletions src/app/shared/state/movie.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { createEntityAdapter, EntityState } from "@ngrx/entity";
import { Movie } from "../models/movie.model";
import { MovieApiActions, MoviesPageActions } from "src/app/movies/actions";
import { createSelector } from "@ngrx/store";

const adapter = createEntityAdapter({
selectId: (movie: Movie) => movie.id,
sortComparer: (a: Movie, b: Movie) =>
a.name.localeCompare(b.name)
sortComparer: (a: Movie, b: Movie) => a.name.localeCompare(b.name)
});

export interface State extends EntityState<Movie> {
@@ -22,31 +22,40 @@ export function reducer(
): State {
switch (action.type) {
case MoviesPageActions.enter.type: {
return {...state, activeMovieId: null};
return { ...state, activeMovieId: null };
}

case MoviesPageActions.selectMovie.type: {
return {...state, activeMovieId: action.movieId};
return { ...state, activeMovieId: action.movieId };
}

case MoviesPageActions.clearSelectedMovie.type: {
return {...state, activeMovieId: null};
}
return { ...state, activeMovieId: null };
}

case MovieApiActions.loadMoviesSuccess.type: {
return adapter.addAll(action.movies, state);
}

case MovieApiActions.createMovieSuccess.type: {
return adapter.addOne(action.movie, {...state, activeMovieId: action.movie.id});
return adapter.addOne(action.movie, {
...state,
activeMovieId: action.movie.id
});
}

case MovieApiActions.updateMovieSuccess.type: {
return adapter.updateOne({id: action.movie.id, changes: action.movie}, {...state, activeMovieId: action.movie.id});
return adapter.updateOne(
{ id: action.movie.id, changes: action.movie },
{ ...state, activeMovieId: action.movie.id }
);
}

case MovieApiActions.deleteMovieSuccess.type: {
return adapter.removeOne(action.movieId, {...state, activeMovieId: null});
return adapter.removeOne(action.movieId, {
...state,
activeMovieId: null
});
}

default: {
@@ -56,4 +65,17 @@ export function reducer(
}

export const { selectEntities, selectAll } = adapter.getSelectors();
export const selectActiveMovieId = (state: State) => state.activeMovieId;
export const selectActiveMovieId = (state: State) => state.activeMovieId;
export const selectActiveMovie = createSelector(
selectEntities,
selectActiveMovieId,
(entities, activeMovieId) => entities[activeMovieId]
);
export const selectEarningsTotal = createSelector(
selectAll,
movies =>
movies.reduce(
(total, movie) => total + parseInt(`${movie.earnings}`, 10) || 0,
0
)
);
22 changes: 11 additions & 11 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NgRx Workshop</title>
<base href="/">
<head>
<meta charset="utf-8" />
<title>NgRx Workshop</title>
<base href="/" />

<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
11 changes: 6 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { AppModule } from "./app/app.module";
import { environment } from "./environments/environment";

if (environment.production) {
enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
3 changes: 1 addition & 2 deletions src/polyfills.ts
Original file line number Diff line number Diff line change
@@ -55,8 +55,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.

import "zone.js/dist/zone"; // Included with Angular CLI.

/***************************************************************************************************
* APPLICATION IMPORTS
18 changes: 9 additions & 9 deletions src/styles.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* You can add global styles to this file, and also import other style files */
@import 'https://fonts.googleapis.com/icon?family=Material+Icons';
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
@import "https://fonts.googleapis.com/icon?family=Material+Icons";
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";

html {
height: 100%;
@@ -45,8 +45,8 @@ mat-sidenav a {
font-weight: 400;
line-height: 47px;
text-decoration: none;
-webkit-transition: all .3s;
transition: all .3s;
-webkit-transition: all 0.3s;
transition: all 0.3s;
padding: 0 16px;
position: relative;
}
@@ -66,21 +66,21 @@ table {
border-spacing: 0;
margin: 0 0 32px;
width: 100%;
box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24), 0 0 2px rgba(0, 0, 0, 0.12);
}

th {
font-size: 16px;
font-weight: 400;
padding: 13px 32px;
text-align: left;
color: rgba(0, 0, 0, .54);
background: rgba(0, 0, 0, .03);
color: rgba(0, 0, 0, 0.54);
background: rgba(0, 0, 0, 0.03);
}

td {
color: rgba(0, 0, 0, .54);
border: 1px solid rgba(0, 0, 0, .03);
color: rgba(0, 0, 0, 0.54);
border: 1px solid rgba(0, 0, 0, 0.03);
font-weight: 400;
padding: 8px 30px;
}