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 DOM

Describes 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 Elements

This document describes the method for enabling the author to define and use new types of DOM elements in a document.

2016-02-25

HTML Imports

This 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

  1. Merge the SearchRes with the Current EntitiesMap (Array)
  2. Merge the SearchRes ids with the Current ids
  3. 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

  • 774