Antal Andrei
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.
Â
Some smart developer
KEEPING THE UI IN SYNC WITH THE STATE IS HARD!
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,
}
...
}
}
new state
Â
ACTION
STATE
COMPONENT
REDUCER
STORE
dispatches
sent to
returns new
update -> render
A reactive state management library
Main benefits
ngrx/store is a package that we can install with the CLI
ng add @ngrx/store
Other packages augment the functionalities of the library
Responsibilities of a component
SMART Components
(How things work?)
DUMB Components
(How things look?)
inputs
outputs
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 { booksReducer } from './store/books';
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ books: booksReducer })
]
})
export class AppModule {}
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({
type: 'ADD_BOOK',
payload: new Book(title, author))
});
}
}
import { createAction, props } from '@ngrx/store';
import { Book } from './model/book';
export const loadBooks = createAction(
'[Books] Load books'
);
export const loadBooksSuccess = createAction(
'[Books] Load books success',
props<Book>()
);
'[Source] Event'
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(
addBook(new Book(title, author))
);
}
}
Component
Action
Reducer
State
dispatch
new state
selector
(render)
STORE
sent to
import * as BookActions from '../actions';
import { createReducer, on, Action } from '@ngrx/store';
interface BooksState {
books: Book[];
loading: boolean;
}
const initialState: BooksState = {
books: [],
loading: false,
};
const booksReducer = createReducer(
initialState,
on(BookActions.loadBooks, (state) => ({
...state,
loading: true,
})),
on(BookActions.addBook, (state, book) => ({
...state,
books: [...state.books, book],
}))
);
export function reducer(state: BooksState | undefined, action: Action) {
return booksReducer(state, action);
}
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { reducer } from './store/books.reducer';
@NgModule({
declarations: [],
imports: [StoreModule.forFeature('books', reducer)]
})
export default class BooksModule { }
...
import BooksModule from './books/books.module';
@NgModule({
imports: [ BrowserModule, StoreModule.forRoot({}), BooksModule ],
...
})
export class AppModule { }
import { createFeatureSelector, createSelector } from '@ngrx/store';
export const getBooksState = createFeatureSelector<BookState>('books');
export const getAllBooks =
createSelector(
getBooksState,
(booksState: BookState) => booksState.data
);
export const getReadBooks =
createSelector(
getAllBooks,
(books: Book[]) => books.filter(book => book.read === true)
);
{
data: [
{ author: 'Author 1', name: 'Book 1', read: true },
{ author: 'Author 2', name: 'Book 2', read: false },
{ author: 'Author 3', name: 'Book 3', read: false }
],
loading: false
}
[
{ author: 'Author 1', name: 'Book 1', read: true },
{ author: 'Author 2', name: 'Book 2', read: false },
{ author: 'Author 3', name: 'Book 3', read: false }
]
[{ author: 'Author 1', name: 'Book 1', read: true }]
import { BooksState, getReadBooks } from './books.reducers';
@Component({
selector: 'books',
template: `
<div class="book" *ngFor="let book of books$ | async">
<span>{{book.title}}</span> - <span>{{book.author}}</span>
</div>
`
})
class BooksComponent {
public books$: Observable<Book[]>;
constructor(private store: Store<BooksState>) { }
ngOnInit() {
this.books$ = this.store.pipe(select(getReadBooks));
}
}
interface Book {
id: number;
title: string;
author: string;
read: boolean;
}
interface BookState {
data: Book[];
loading: false;
loaded: false;
}
on(BooksActions.addBook, (state, book) => ({
...state,
data: [...state.data, book]
})
on(BooksActions.removeBook, (state, bookId) => ({
...state,
data: state.data.filter(book => book.id !== bookId)
})
on(BooksActions.readBook, (state, bookId) => {
const bookIndex =
state.data.findIndex(book => book.id === bookId);
return {
...state,
data: [
...state.data.slice(0, bookIndex),
{
...state.data[bookIndex],
read: true
},
...state.data.slice(bookIndex+1),
]
};
})
on(BooksActions.readBook, (state, bookId) => {
const bookIndex =
state.data.findIndex(book => book.id === action.payload);
return {
...state,
data:
state.data.slice(0, bookIndex)
.concat({...state.data[bookIndex], read: true})
.concat(state.data.slice(bookIndex+1))
};
})
[
{id: 1, ...},
{id: 2, ...},
{id: 3, ...}
]
{
1: {id: 1, ...},
2: {id: 2, ...},
3: {id: 3, ...}
}
import { createEntityAdapter } from '@ngrx/entity';
const bookAdapter = createEntityAdapter<Book>();
import { EntityState } from '@ngrx/entity';
export interface BookState extends EntityState<Book> {
loading: boolean,
}
const initialState: BookState = bookAdapter.getInitialState({
loading: true,
});
ng add @ngrx/entity
const booksReducer = createReducer(
initialState,
on(BooksActions.addBooks, (state, {books}) => ({
...state,
books: bookAdapter.addAll(books, state.books)
})),
on(BooksActions.addBook, (state, book) => ({
...state,
books: adapter.addOne(book, state.books)
})),
on(BooksActions.removeBook, (state, book) => ({
...state,
books: bookAdapter.removeOne(book.id, state.books)
})),
on(BooksActions.readBook, (state, book) => ({
...state,
books: bookAdapter.updateOne(
{ id: book.id, changes: { read: true } },
state.books
);
})),
on(BooksActions.bookLoaded, (state) => ({
...state,
loaded: true
})),
);
Component
Action
Reducer
State
dispatch
new state
render
STORE
sent to
Command Query Separation
Dispatcher
State
Commands
Queries
Actions
Selectors
UI
export class AddBook implements Action {
type = BookActions.ADD_BOOK;
constructor(public payload: Book) {}
}
export class SetBooks implements Action {
type = BookActions.SET_BOOKS;
constructor(public payload: Book[]) {}
}
export class BooksLoadedSuccess implements Action {
type = BooksActions.FETCH_SUCCESS;
constructor(public payload: any) {}
}
LOAD_BOOKS
LOADER_VISIBLE
FETCH_BOOKS
FETCH_SUCCESS
SET_BOOKS
SET_ERROR
FETCH_ERROR
LOADER_HIDDEN
LOADER_HIDDEN
LOAD_BOOKS
LOADER_VISIBLE
FETCH_BOOKS
FETCH_SUCCESS
SET_BOOKS
SET_ERROR
FETCH_ERROR
LOADER_HIDDEN
LOADER_HIDDEN
(command)
(command)
(document)
(document)
(document)
(document)
(document)
(event)
(event)
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
fetch
receive
dispatch
import { Injectable } from '@angular/core';
import { Actions, createEffect } from '@ngrx/effects';
import { of } from 'rxjs/observable/of';
import { mergeMap, map, catchError } from 'rxjs/operators';
import * as BookActions from './actions';
@Injectable()
class BooksEffects {
constructor(private actions$: Actions, private http: Http) {}
addBook$ = createEffect(() =>
this.actions$.pipe(ofType(BookActions.addBook).pipe(
mergeMap(book =>
this.http.post('api/book', book)
.pipe(
map(responseBook => BookActions.bookLoadSuccessfuly(responseBook)),
catchError(error => of(BookActions.bookLoadFail(error)),)
)
))
);
}
"message bus"
ng add @ngrx/effects
export interface BookState {
books: Book[];
loading: boolean;
error: boolean;
}
export const initialState: BookState = {
books: [],
loading: false,
error: undefined,
};
const moviesReducer = createReducer(
initialState,
on(BooksActions.addBook, (state) => ({ ...state, loading: true })),
on(BooksActions.bookAddSuccess, (state, book) => ({
...state,
loading: false,
error: undefined,
books: [...state.books, book],
})),
on(BooksActions.bookAddFail, (state, error) => ({
...state,
loading: false,
error,
}))
);
@Injectable()
class BooksEffects {
constructor(private actions$: Actions, private http: Http) {}
addBook$ = createEffect(() =>
this.actions$.ofType(BooksActions.addBook)
.pipe(
concatMap(book => this.http.post('api/book', book)
.pipe(
map(() => BookActions.bookAddSuccessful(book))
)
);
}
const moviesReducer = createReducer(
initialState,
on(BookActions.bookAddSuccessful, (state, book) => ({
...state,
books: [...state.books, book],
}))
)
This action is not handled in the reducer, it's dispatched just for the effect
...
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { reducer } from './store/books.reducer';
import BooksEffects from './store/books.effects';
@NgModule({
...
imports: [
...
StoreModule.forFeature('books', reducer),
EffectsModule.forFeature([BooksEffects])
],
providers: [BooksEffects]
})
export default class BooksModule {}
Effect service must also be declared in the providers array
Credits to:
class BooksEffects {
constructor(private actions: Actions, private env: Env) {}
addBook = createEffect(() =>
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 BooksEffects {
constructor(private actions: Actions) {}
addBook = createEffect(() =>
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) {}
addBook$ = createEffect(() =>
this.actions$.ofType('ADD_BOOK').pipe(
map(add => ({
action: 'ADD_BOOK_BY_USER',
payload: {...add.payload, user: this.currentUser}
}))
)
);
}
NgRx main benefits
NgRx - things to mind