ngBucharest & BJUG meetup - 27.06.2018 @Netcentric
Antal Andrei
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.
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
yarn 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 { Action } from '@ngrx/store';
import { Book } from './model/book';
export enum BookActionTypes {
LOAD_BOOKS = '[Books] Load books',
ADD_BOOK = '[Books] Add one book'
}
export class LoadBooks implements Action {
readonly type = BookActionTypes.LOAD_BOOKS;
}
export class AddBook implements Action {
readonly type = BookActionTypes.ADD_BOOK;
constructor(public payload: 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(
new AddBook(new Book(title, author))
);
}
}
Component
Action
Reducer
State
dispatch
new state
selector
(render)
STORE
sent to
import { LOAD_BOOKS, ADD_BOOK } from '../actions';
interface BooksState {
books: Book[];
loading: boolean;
}
const initialState: BooksState = {
books: [],
loading: false,
};
export function reducer(state = initialState, action): BooksState {
switch (action.type) {
case LOAD_BOOKS: {
return {
...state,
loading: true,
};
}
case ADD_BOOK: {
return {
...state,
books: [...state.books, action.payload],
};
}
}
return state;
}
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 { }
@Component({
selector: 'books',
template: `
<div class="book" *ngFor="let book of books">
<span>{{book.title}}</span> - <span>{{book.author}}</span>
</div>
`
})
class BooksComponent {
public books: Book[];
constructor(private store: Store<any>) { }
ngOnInit() {
this.store
.select('books')
.subscribe(data => this.books = data.books)
}
}
interface BooksState {
books: Book[];
loading: boolean;
}
interface Book {
title: string;
author: string;
read: boolean;
}
@Component({
selector: 'books',
template: `
<div class="book" *ngFor="let book of books">
<span>{{book.title}}</span> - <span>{{book.author}}</span>
</div>
`
})
class BooksComponent {
public books: Book[];
constructor(private store: Store<any>) { }
ngOnInit() {
this.store
.select('books')
.subscribe(data =>
this.books = data.books.filter(book => !book.read)
)
}
}
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.select(getReadBooks)
}
}
interface Book {
id: number;
title: string;
author: string;
read: boolean;
}
interface BookState {
data: Book[];
loading: false;
loaded: false;
}
case BookActions.ADD_BOOK: {
return {
...state,
data: [...state.data, action.payload]
}
}
case BookActions.REMOVE_BOOK: {
return {
...state,
data: state.data.filter(book => book.id !== action.payload)
}
}
case BookActions.READ_BOOK: {
const bookIndex =
state.data.findIndex(book => book.id === action.payload);
return {
...state,
data: [
...state.data.slice(0, bookIndex),
{
...state.data[bookIndex],
read: true
},
...state.data.slice(bookIndex+1),
]
};
}
case BookActions.READ_BOOK: {
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,
});
yarn add @ngrx/entity
function reducer( state = initialState, action ): BookState {
switch (action.type) {
case BookActions.ADD_BOOKS: {
return bookAdapter.addAll(action.payload.books, state);
}
case BookActions.ADD_BOOK: {
return bookAdapter.addOne(action.payload.book, state);
}
case BookActions.REMOVE_BOOK: {
return bookAdapter.removeOne(action.payload.id, state);
}
case BookActions.READ_BOOK: {
return bookAdapter.updateOne(
{ id: action.payload.id, changes: { read: true } }, state
);
}
case BookAction.BOOK_LOADED: {
return { ...state, loaded: true };
}
}
return state;
}
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, Effect } from '@ngrx/effects';
import { of } from 'rxjs/observable/of';
import { switchMap, map, catchError } from 'rxjs/operators';
@Injectable()
class BooksEffects {
constructor(private actions$: Actions, private http: Http) {}
@Effect()
addBook$ = this.actions$.ofType('ADD_BOOK').pipe(
switchMap(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"
yarn add @ngrx/effects
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 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(() => ({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
...
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) {}
@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 BooksEffects {
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}
}))
);
}
DEMO
NgRx main benefits
NgRx - things to mind