Voxxed Days 2018 - Bucharest
Antal Andrei
slides.com/andreiantal/reactive_state_ng/
organizer of ngBucharest
@ngBucharest
groups/angularjs.bucharest
Module
Config
Routes
$scope
controller
service
view
directive
Module
Routes
Service
Component
Metadata - decorator
ES6 Class
Module
Routes
Service
Component
import { Component, OnInit } from '@angular/core';
import { ItemsService, Item } from '../shared';
export class ItemsComponent implements OnInit {}
Modules export things that other modules can import
@NgModule({
declarations: [
AppComponent,
ItemsComponent,
ItemsListComponent,
ItemDetailComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
Ng2RestAppRoutingModule
],
providers: [ItemsService],
bootstrap: [AppComponent]
})
export class AppModule { }
define view classes that are available to the module - the other view classes in the module
define a list of other modules that the current module needs
define a list of services the module makes available
defines the component that should be bootstrapped
Module
Routes
Service
Component
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ItemsComponent } from './items/items.component';
const routes: Routes = [
{path: '', redirectTo: '/items', pathMatch: 'full' },
{path: 'items', component: ItemsComponent},
{
path: 'widgets',
loadChildren: '../modules/books/books.module#BooksModule',
},
{path: '**', redirectTo: '/items', pathMatch: 'full'}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule { }
Super-easy lazy load of routes
Module
Routes
Service
Component
Decorator necessary only if service injects other providers
import { Injectable } from '@angular/core';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
const BASE_URL = 'http://localhost:3000/items/';
@Injectable()
export class ItemsService {
constructor(private http: Http) {}
loadItems() {
return this.http.get(BASE_URL)
.map(res => res.json())
.toPromise();
}
}
Module
Routes
Service
Component
Metadata - the @Component decorator
Class
Template
Bindings
events
properties
import { Component } from '@angular/core';
@Component({
selector: 'app-component',
template: `
<h1> Hello, {{ name }}! </h1>
<button (click)="changeName()">CHANGE NAME</button>
`,
})
export class AppComponent {
name = 'Andrei';
logGreet() {
this.name = 'Andrew';
console.log(`Name has been changed to ${ this.name }`);
}
}
Module
Component
Provider
Directive
Pipe
metadata
metadata
metadata
metadata
metadata
class
class
class
class
class
(Data projection)
const bookTitle = "Alchemy for beginners";
const chaptersTitles = [ "Equipment" ]
let book = {
title: bookTitle,
chapters: chapterTitles
}
.....
Strings, numbers, arrays, objects etc.
Document Object Model
(DOM)
(rendering)
State (in the broadest sense) is the complex of all of the values held by the application as properties or variables at any given time, and the application is conceived of as acting by executing methods/functions that change the current state to another state.
VIEW
STORE
ACTION
DISPATCHER
ACTION
data flow
The 3 main principles:
// initial state object
const state = {
books: [],
movies: [],
}
// book addition action object
const action = {
type: 'ADD_BOOK',
payload: {
id: 123,
title: '1984',
author: 'George Orwell',
read: false,
},
};
// reducer function
function booksReducer(state, action) {
switch(action.type) {
case 'ADD_BOOK':
const newBook = action.payload;
const books = [
...state.books,
newBook
];
return {
...state,
books,
}
break;
...
}
}
new state
ACTION
STATE
COMPONENT
REDUCER
STORE
dispatches
sent to
returns new
update -> render
A reactive state management library
Main benefits
npm install @ngrx/store --save
Other packages augment the functionalities of the library
Container components
Presentational components
Container Component
Dumb Component
Dumb Component
STORE
@Output
@Output
@Input
@Input
Dispatch
Select
import { NgModule } from '@angular/core'
import { StoreModule } from '@ngrx/store';
import { todosReducer } from './todos';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ todos: todosReducer })
]
})
export class AppModule {}
import { Action } from '@ngrx/store';
import { Book } from './model/book';
export const LOAD_BOOKS = '[Books] Load books';
export const ADD_BOOK = '[Books] Add book';
export class LoadBooks implements Action {
type = LOAD_BOOKS;
}
export class AddBook implements Action {
type = ADD_BOOK;
constructor(public payload: Book) {}
}
export class AddBook implements Action {
type = 'ADD_BOOK';
constructor(public payload: Book) {}
}
export class Books implements Action {
type = 'BOOKS';
constructor(public payload: Book[]) {}
}
export class BooksLoaded implements Action {
type = 'BOOKS_LOADED';
constructor(public payload: any) {}
}
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AddBook } from './store.actions';
import { Book } from './model/book';
@Component({
selector: 'books',
templateUrl: './books.component.html'
})
class BooksComponent {
constructor(private store: Store<any>) {}
addBook(title: string, author: string) {
this.store.dispatch(
new AddBook(new Book(title, author))
);
}
}
export interface BookaState {
books: Book[];
loading: boolean;
}
export const initialState: BookaState = {
books: [],
loading: false,
};
export function reducer(state = initialState, action): BookaState {
switch (action.type) {
case LOAD_BOOKS: {
return {
...state,
loading: true,
};
}
case ADD_BOOK: {
return {
...state,
books: [...state.books, action.payload],
};
}
}
return state;
}
@Component({
selector: 'books',
template: `
<div class="book" *ngFor="let book of books$ | async">
<span>{{book.title}}</span> - <span>{{book.author}}</span>
</div>
`
})
class TodosComponent {
public books$: Observable<Book[]>;
constructor(private store: Store<any>) {
this.books$ = store.select('books');
}
...
}
export interface BookState {
data: Book[];
loading: boolean;
error: any;
}
export interface BooksState {
books: BookState;
}
export const getBooks = (state: BookState): Book[] => state.data;
export const getProductsState
= createFeatureSelector<BooksState>('booksFeature');
export const getBooksState = createSelector(
getProductsState,
(state: BooksState): BookState => state.books,
);
export const getAllBooks = createSelector<BooksState, BookState, Book[]>(
getBooksState,
getBooks,
);
import { BooksState, getAllBooks } from './books.reducers';
@Component({
selector: 'books-component',
template: `
<book-item *ngFor="let book of books$ | async" [book]="book">
</book-item>
`
})
export class BooksComponent implements OnInit {
books$: Observable<Book[]>;
constructor(private store: Store<BooksState>) {}
ngOnInit() {
this.books$ = this.store.select(getAllBooks);
}
}
Component
Action
Reducer
State
dispatch
new state
render
STORE
sent to
Component
Action
Reducer
State
dispatch
new state
render
STORE
sent to
EFFECTS
EFFECT
SERVICE
Action
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { of } from 'rxjs/observable/of';
import { concatMap, map, catchError } from 'rxjs/operators';
@Injectable()
class BooksEffects {
constructor(private actions$: Actions, private http: Http) {}
@Effect()
addBook$ = this.actions$.ofType('ADD_BOOK').pipe(
concatMap(book =>
this.http.post('api/book', book)
.pipe(
map(responseBook =>
({type: 'BOOK_ADDED', payload: responseBook}),
catchError(error =>
of({type: 'BOOK_ADD_FAIL', payload: error}))
)
)
);
}
"message bus"
export interface BookState {
books: Book[];
loading: boolean;
error: boolean;
}
export const initialState: BookState = {
books: [],
loading: false,
error: undefined,
};
export function reducer(state = initialState, action): BookState {
switch (action.type) {
case ADD_BOOK: {
return { ...state, error: undefined, loading: true };
}
case BOOK_ADDED: {
return { ...state, loading: false, books: [...state.books, action.payload] };
}
case BOOK_ADD_FAIL: {
return { ...state, error: action.payload, loading: false };
}
}
return state;
}
@Injectable()
class TodosEffects {
constructor(private actions$: Actions, private http: Http) {}
@Effect()
addBook$ = this.actions$.ofType('ADD_BOOK')
.pipe(
concatMap(book => this.http.post('api/book', book)
.pipe(
map(() => ({type: 'BOOK_ADDED', payload: book}))
)
);
}
function booksReducer(state: Book[] = [], action: any): Book[] {
switch (action.type) {
case('BOOK_ADDED') {
return [...books, action.payload];
}
}
return todos;
}
This action is not handled in the reducer, it's dispatched just for the effect
Credits to:
class BooksEffects {
constructor(private actions: Actions, private env: Env) {}
@Effect()
addBook = this.actions.typeOf('ADD_BOOK').pipe(map(addBook => {
if (this.env.confirmationIsOn) {
return ({type: 'ADD_BOOK_WITH_CONFIRMATION', payload: addBook.payload});
} else {
return ({type: 'ADD_BOOK_WITHOUT_CONFIRMATION', payload: addBook.payload});
}
});
}
class TodosEffects {
constructor(private actions: Actions) {}
@Effect()
addBook = this.actions.typeOf('REQUEST_ADD_BOOK').pipe(flatMap(add => [
{type: 'ADD_BOOK', payload: add.payload},
{type: 'LOG_OPERATION', payload: {loggedAction: 'ADD_BOOK', payload: add.payload}}
]);
}
class BooksEffects {
constructor(private actions$: Actions, private currentUser: User) {}
@Effect()
addBook$ = this.actions$.ofType('ADD_BOOK').pipe(
map(add => ({
action: 'ADD_BOOK_BY_USER',
payload: {...add.payload, user: this.currentUser}
}))
);
}