Application Architecture Angular2

Application Architecture Angular2
Prerequisites

- Web Components (Angular / Polymer)
- TypeScript
- Observable
- RxJs
- Redux
Web Components

http://webcomponents.org/
Web Components

Web Components

| 2016-06-24 |
Shadow DOMDescribes a method of establishing and maintaining functional boundaries between DOM subtrees and how these subtrees interact with each other within a document tree. |
| 2016-06-21 |
Custom ElementsThis document describes the method for enabling the author to define and use new types of DOM elements in a document. |
| 2016-02-25 |
HTML ImportsThis document defines a way to include and reuse HTML documents in other HTML documents. |
Web Components

Polymer

https://www.polymer-project.org

Polymer


Polymer

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-styles/paper-styles.html">
<link rel="import" href="bower_components/iron-icon/iron-icon.html">
<link rel="import" href="bower_components/iron-icons/iron-icons.html">
<link rel="import" href="bower_components/iron-icons/maps-icons.html">
<link rel="import" href="bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="bower_components/paper-radio-group/paper-radio-group.html">
<link rel="import" href="bower_components/paper-radio-button/paper-radio-button.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-input/paper-input.html">
<link rel="import" href="bower_components/paper-item/paper-item.html">
<link rel="import" href="bower_components/paper-card/paper-card.html">
<link rel="import" href="bower_components/paper-spinner/paper-spinner.html">
<paper-card heading="{{title}}">
<div class="subtitle">{{subtitle}}</div>
<img src="thumbnail"/>
<div class="card-content">{{description}}</div>
<paper-button>Remove from collection</paper-button>
<paper-button>Add to collection</paper-button>
</paper-card>Angular 2

https://angular.io
Angular 2

@Component({
moduleId: module.id,
selector: 'book-preview',
directives: [NgSwitch],
templateUrl: 'book.preview.html',
styleUrls: ['book.preview.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BookPreviewComponent {
public collectionAdd$ = new Subject<AddOutput>().map(e =>Object.assign(new AddOutput(), e));
public collectionRemove$ = new Subject<RemoveOutput>().map(e =>Object.assign(new RemoveOutput(), e));
@Input() book: Book;
@Output() collectionToggle$: Observable<AddOutput|RemoveOutput> = this.collectionAdd$
.merge(this.collectionRemove$);
constructor() {
console.log('BookPreviewComponent')
}
...
}
<book-preview (collectionToggle$)="onCollectionToggle($event)"></book-preview>TypeScript

http://www.typescriptlang.org/

TypeScript

class Animal {
constructor(public name: string) { }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);Observable

Proposal to introduces
Observable type to the ECMAScript
const search: Observable<SearchOutput> = this.keyup$
.debounceTime(300)
.map(event => (event.target as HTMLInputElement).value)
.distinctUntilChanged();- Enables the resolution of complex problem in an elegant, clean and simple way (ie: Retrys and Debounce)
- Removes States from Components
- Enables the writing of Functional Reactive Application with uni-directional data flow
RxJs

http://reactivex.io/

RxJs

Observable Toolkit


http://rxmarbles.com/
Redux

https://github.com/reactjs/redux

A predictable state container
for JavaScript apps.
Concepts

- Immutability
- Uni-Directional Data Flow
- Fonctional Reactive Programming
- Data Store
- Smart/Dumb Components
- Inter-Components Data-Transfer Patterns
- Reducers / Projections / Effects
Attention FRP Architecture is Optional
Polymer is a UI Library, there are other
UI Libraries that you can choose from
Elements

- Actions
- App
- Components
- Effects
- Models
- Pages
- Projections
- Reducers
- Services
- Bootstrap
App Structure
Page (Smart)
Component (Dumb)
Smart / Dumb Components


Smart Component (Page 1)
collectionCnt$
router links

Smart Component
(Page 2)
Action + / - Collection
searchQuery$
searchPending$
book$

Action + / - Collection
book$
Smart Component
(Page 3)

Dumb
Component

Smart Components
Has a reference to the Store
Dumb Components
Primarly getters and
no ref to the Store


Data Flow

What is the Store ?

{
books: ...,
search: ...,
collection: ...,
}A Store is an arbitrary
Object Structure
That will generate
snap-shots of your current App State
---(snap-shot)---(snap-shot)---(snap-shot)---(snap-shot)---(snap-shot)->
But what goes
at the place of these
mysterious 3 small dots
???
Reducers
---(snap-shot)---(snap-shot)---(snap-shot)---(snap-shot)---(snap-shot)->
{
books: bookReducer,
search: searchReducer,
collection: collectionReducer,
}Like a DB a Store
has a Structure and Content
The structure is the top level of the App State
The Content is generated by the Reducers
{
books: ...,
search: ...,
collection: ...,
}{
books: data <== bookReducer,
search: data <== searchReducer,
collection: data <== collectionReducer,
}books:{
entities:{
0BSOg0oHhZ0C:{
id: string;
isInCollection:boolean; <----- Attention
volumeInfo: {
title: string;
subtitle: string;
authors: string[];
publisher: string;
publishDate: string;
description: string;
averageRating: number;
ratingsCount: number;
imageLinks: {
thumbnail: string;
smallThumbnail: string;
};
};
},
0fdgsdfgsdfg:{...},
...
ids:[
"0BSOg0oHhZ0C",
"0fdgsdfgsdfg",
"sD7HBwAAQBAJ",
"Vf32PZXJ2gMC"
...
]
}
BookReducer Data Output
collection:{
ids:[
"0BSOg0oHhZ0C",
"0fdgsdfgsdfg",
"sD7HBwAAQBAJ",
"Vf32PZXJ2gMC"
...
]
}
CollectionReducer Data Output
collection:{
ids:[
"0BSOg0oHhZ0C",
"0fdgsdfgsdfg",
"sD7HBwAAQBAJ",
"Vf32PZXJ2gMC"
...
]
}
SearchReducer Data Output
Rob Wormald
Data Flow

What is a Reducer ?

A reducer is a function that create a new "Table State"
(doing so will creates a new "App State")
bookReducer
{
books: bookReducer,
search: searchReducer,
collection: collectionReducer,
}books:{
entities:{
0BSOg0oHhZ0C:{
id: string;
isInCollection:boolean; <----- Attention
volumeInfo: {
title: string;
subtitle: string;
authors: string[];
publisher: string;
publishDate: string;
description: string;
averageRating: number;
ratingsCount: number;
imageLinks: {
thumbnail: string;
smallThumbnail: string;
};
};
},
0fdgsdfgsdfg:{...},
...
ids:[
"0BSOg0oHhZ0C",
"0fdgsdfgsdfg",
"sD7HBwAAQBAJ",
"Vf32PZXJ2gMC"
...
]
}
BookReducer Details
(Table = Books, Column = entities, ids)
Objectif
The search return an Array of Books
Context
- Merge the SearchRes with the Current EntitiesMap (Array)
- Merge the SearchRes ids with the Current ids
- Create a new EntitiesMap from the merge Books
ie: Update bookReducer
Reducer Example
case BookActions.SEARCH_COMPLETE:
{
const searchRes: Book[] = action.payload;
//get the entities values as an array
const currentBooks: Book[] = Object.keys(state.entities).map(k => state.entities[k]);
//merge of search and current books
const newBooks: Book[] = _.uniqBy([...currentBooks, ...searchRes], 'id');
//same for ids
const newBookIds: string[] = _.uniqBy([...state.ids, ...newBooks.map(book => book.id)]);
const newEntitiesMap = newBooks.reduce((entities: { [id: string]: Book }, book: Book) => {
return Object.assign(entities, {[book.id]: book});
}, {});
return {
ids: newBookIds,
entities: newEntitiesMap
};
}Data Flow

What is an Action

Actions Pattern
Event {Type, Payload} that trigger a Reducer and/or an Effect
static SEARCH = '[Book] Search';
search(query:string):Action {
return {
type: BookActions.SEARCH,
payload: query
};
}
static SEARCH_COMPLETE = '[Book] Search Complete';
searchComplete(results:Book[]):Action {
return {
type: BookActions.SEARCH_COMPLETE,
payload: results
};
}What is an Effect

Effects
(Short for Side Effects)
A chain of actions with side effects
and the App State as Scope

@Effect() search$ = this.appState$
.whenAction(BookActions.SEARCH)
.map<string>(toPayload)
.switchMap(query => this.googleBooks.searchBooks(query)
.map(books => this.bookActions.searchComplete(books))
.catch(() => Observable.of(this.bookActions.searchComplete([])))
);
//Search Reducer set pending to true
case BookActions.SEARCH: {
const query = action.payload;
return Object.assign(state, {
query,
pending: true
});
}
//Search Reducer set pending to false
case BookActions.SEARCH_COMPLETE: {
const books: Book[] = action.payload;
return {
ids: books.map(book => book.id),
pending: false,
query: state.query
};
}Pattern For Service Call
//Book Reducer
case BookActions.SEARCH_COMPLETE: {
const searchRes: Book[] = action.payload;
const currentBooks = Object.keys(state.entities)
.map(k => state.entities[k]);
//merge books and current searchRes
const newBooks = _.uniqBy(
[...currentBooks ,
...searchRes],
'id');
const newBookIds = _.uniqBy(
[...state.ids,
...newBooks.map(book => book.id)
]);
return {
ids: newBookIds,
entities: createEntitiesMap(newBooks)
};
} @Effect() Add$ = this.appState$
.whenAction(CollectionActions.ADD_TO_COLLECTION)
.map((o: {action: Action, state: AppState}) => {
return o.state.books.entities[o.action.payload];
})
.map(entity => this.collectionActions.addToCollectionComplete(entity));
//Book Reducer Update isInCollection: true on the selected Book
case CollectionActions.ADD_TO_COLLECTION: {
const bookId = action.payload;
const newBook = Object.assign(state.entities[bookId], {isInCollection: true});
const newBooks = applyNewBookToBooks(state, newBook);
return {
ids: state.ids,
entities: getNewEntities(newBooks)
};
}
//Collection Reducer add the Book id to the collection.ids
case CollectionActions.ADD_TO_COLLECTION_COMPLETE:
const addBookId = action.payload.id;
return {
ids: _.uniqBy([...state.ids, addBookId]),
};Pattern For Chaining Actions
Data Flow

How do I trigger an Action ?
And from where ?

Only trigger Actions in
Smart Components
this.store.dispatch(this.collectionActions.addToCollection(e.id));
this.store.dispatch(this.collectionActions.removeFromCollection(e.id));
Inject Store in Smart Components

Don't let dumbs talk to each other



Always put a smart in-between
Ok now I know how to create new App State

How do I access
my App State

Data Flow

Projections
export function getSearchResults() {
return (state$:Observable<AppState>)=>
state$
.map((s:AppState)=> {
let bookIds = s.search.ids;
let entities = s.books.entities;
return bookIds.map(id => entities[id]);
})
}
export function getCollectionCnt() {
return (state$:Observable<AppState>)=>
state$
.map((s:AppState)=>{
return s.collection.ids.length
})
}Access to data from smart component
with named function with the App State scope
this.books$ = store.let(getSearchResults());
this.collectionCnt$ = store.let(getCollectionCnt());Data Flow

What about the dumb components


Inter-Components
Data-Transfer Patterns
- Shared Service
- Events Bubbling
- Pub/Sub
- Contracts
Contracts

Dumb Components
@Input & @Output

Book-find
@Input() searchQuery$
@Input() searchPending$
@Input() books$
Book
@Input() book
@Output() collectionToggle$
Book-List
@Input() books
@Output() collectionToggle$
Book Component (dumb)

public collectionAdd$ = new Subject<AddOutput>().map(e => Object.assign(new AddOutput(), e));
public collectionRemove$ = new Subject<RemoveOutput>().map(e => Object.assign(new RemoveOutput(), e));
@Input() book: Book;
@Output() collectionToggle$: Observable<AddOutput|RemoveOutput> = this.collectionAdd$
.merge(this.collectionRemove$);
get id() {
return this.book.id;
}
get title() {
return this.book.volumeInfo.title;
}
get description() {
return this.book.volumeInfo.description;
}
get inCollection(): boolean {
return !!this.book.inCollection;
}
<paper-card [heading]="title">
<paper-button (click)="collectionRemove$.next(book)">Remove from collection</paper-button>
<paper-button (click)="collectionAdd$.next(book)">Add to collection</paper-button>
</paper-card>Book-List Component (dumb)

@Output() collectionToggle$ = new Subject<AddOutput|RemoveOutput>();
@Input() books: Books;
onCollectionToggle(e: AddOutput|RemoveOutput) {
this.collectionToggle$.next(e);
}
<div class="book-list">
<book (collectionToggle$)="onCollectionToggle($event)"
*ngFor="let book of books" [book]="book"></book>
</div>Book-Find Page (smart)
onCollectionToggle(e) {
if (e instanceof AddOutput) {
this.store.dispatch(this.collectionActions.addToCollection(e.id));
}
if (e instanceof RemoveOutput) {
this.store.dispatch(this.collectionActions.removeFromCollection(e.id));
}
}
...
<book-list [books]="books$ | async"
(collectionToggle$)="onCollectionToggle($event)"></book-list>
...



Application Architecture Ng2
By bretto
Application Architecture Ng2
- 909

