Angular 2 in Action
Solving Real life challenges with angular 2
@ Algotec
Medical Imaging Workstation

Carestream Vue PACS
- 15 years of development
- Assembly, C, C++, C#, VB.net etc..
- Patient/Study list, Numeros Image viewers, Medical applications, reports ....
- Client/server - PACS (server) has data, client does all the processing work

Win32 ----> Web
- Ecosystem (devs, libs...)
- Easy deployment
- Easy to maintain up to date
- Easy to make cross platform
- Easy on hardware demands
- Difficult to keep performance
- Difficult to access hardware
Pros
Cons
Hardware access
- Multiple monitors
- Proprietary professional hardware
- Image/Audio/video hardware acceleration w/ custom code - 3D modeling, voice recognition etc..


Angular 2
- DI
- Components
- Observable streams
- Great performance
- Testing
- Typescript
- Still evolving and changing
- Ecosystem not as rich as Angular 1.x or React etc..

High level Architecture -
Choose not to choose. Use DI
- Desktop (Nw.js) and web versions
- Desktop will be able to automatically use all monitors, access scanner hardware etc.
- Web uses the same code, with some limitations.
- Node.js thread/Shared Worker for server API code to allow multiple tabs with one connection to server
- Dual client rendering pipes - WebGL and native JS (using ASM.js) as well as server rendering- use decided at runtime
- Main App is in UI thread to allow WebGL access
- Additional web workers do processing tasks.
Everything is a stream
Great for complex user interaction
mouseDown.flatMap((ev) => {
return mouseMove.map((ev) =>{
return {
x: ev.clientX,
y: ev.clientY
};
}).pairwise().takeUntil(mouseUp);
}).subscribe(...
Great For async failure managment
ServerConnectionObservable.retryWhen(function (attempts) {
return Rx.Observable.range(1, 3)
.zip(attempts, (i) => i)
.flatMap((i)=>{
console.log("delay retry by " + i + " second(s)");
return Rx.Observable.timer(i * 1000);
});
}).subscribe();
State Managment
- Handle async actions
- Great error handling.
- Great performance.
- Debuggability & predictability.
Needs:
State Managment
- Components are mere event emitters & state presentores
- Containers connect to data , catch component events, and pass them on to action producers.
- Producers create actions and exposes store data as observables
- Handlers deal with action, do async tasks which produce one or more sub actions to dispatcher when state needs to change
- Reducers change state (synchronously)
NgRx+ ---> Flux on steroids
Demo - one simple button click...
Button.component.ts
import {Input, Output, Component, EventEmitter, ChangeDetectionStrategy} from "@angular/core";
import {ButtonModel} from "./../models/button.model.ts";
@Component({
selector: 'alg-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template:`<span>
<button *ngIf="button.isIcon" md-icon-button (click)="onClick()" [disabled]="button.disabled"
[ngClass]="button.getClassList()">
<i [ngClass]="button.getIconsClassList()">{{button.iconText}}</i>
</button>
<button *ngIf="!button.isIcon" md-button (click)="onClick()" [disabled]="button.disabled"
[ngClass]="button.getClassList()">{{button.text}}
</button>
</span>`,
})
export class ButtonComponent {
@Input()
button:ButtonModel;
@Output()
buttonClick:EventEmitter<any> = new EventEmitter(false); //<buttonModel>
onClick() {
this.buttonClick.emit(this.button);
}
}
import {Component, Output, Input, EventEmitter, ChangeDetectionStrategy} from "@angular/core";
import {IButtonsMap, ButtonModel} from "../models/button.model.ts";
import {ButtonComponent} from "./button.component";
import {MdToolbar} from "@angular2-material/toolbar";
import {MdButton} from "@angular2-material/button";
@Component({
selector: 'at-ribbon',
directives: [ButtonComponent, MdToolbar, MdButton],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div class="btn-group btn-group-lg" role="group">
<alg-button *ngFor="let button of buttons" (buttonClick)="buttonClick.emit($event)" [button]="button[1]"></alg-button>
</div>`
// the default iterator of immutableJS will return entries style [key,value] so [1] is the value,
// it could have been nice to iterate over .values() or to deconstruct but angular does not support this for now
})
export class RibbonComponent {
@Input()
buttons:IButtonsMap;
@Output()
buttonClick:EventEmitter<any> = new EventEmitter(false); //<ButtonModel>
//custom event do not bubble: https://github.com/angular/angular/issues/2296;
when this is fixed, we can remove the chaining of buttonClick emitters
}
import {Component, Input, ChangeDetectionStrategy} from "@angular/core";
import {RibbonComponent} from "../components/ribbon.component";
import {RibbonProducer} from "../producer/ribbon.producer.ts";
import {RIBBON_PROVIDERS} from "../ribbon.providers";
@Component({
selector: 'alg-ribbonContainer',
directives: [RibbonComponent],
providers : RIBBON_PROVIDERS,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<at-ribbon [buttons]="ribbon.buttons$ | async" [panelID]="panelID" (buttonClick)="internalButtonClick($event)"></at-ribbon>`
})
export class RibbonContainer {
@Input('panel')
panelID:string;
public constructor(private ribbon:RibbonProducer) {
}
internalButtonClick(buttonClickEvent) {
this.ribbon.ribbonButtonClick(this.panelID, buttonClickEvent);
}
}
import {Store, Dispatcher} from "@ngrx/store";
import {IButtonsMap, ButtonModel} from "./../models/button.model.ts";
import {ribbonActions} from "./../actions/ribbon.actions.ts";
import {Injectable, Inject, Injector} from "@angular/core";
import {Observable} from "rxjs/Observable";
import {RIBBON_HANDLERS} from "../handlers/ribbon.handlers";
import {BaseProducer} from "../../../common/producers/base.producer";
@Injectable()
export class RibbonProducer extends BaseProducer {
buttons$:Observable<IButtonsMap>;
constructor(store:Store<any>, @Inject(RIBBON_HANDLERS) ribbonHandlers, injector:Injector, dispatcher:Dispatcher<any>) {
super(injector, dispatcher, ribbonHandlers);
this.buttons$ = store.select<any>('ribbon').map(ribbonState => ribbonState.buttons);
}
ribbonButtonClick(panelId:string, button:ButtonModel) {
this.$handlersToDispatch({
type: ribbonActions.RIBBON_BUTTON_CLICKED,
payload: {panelId, button}
});
}
}
import {Dispatcher, Action} from "@ngrx/store";
import {Injectable, ReflectiveInjector, Provider} from "@angular/core";
import {IHandler} from "../models/handler.interface";
import {Injector, provide, ResolvedReflectiveProvider} from "@angular/core";
@Injectable()
export class BaseProducer {
private handlers:Array<IHandler>;
private injector:ReflectiveInjector;
constructor(injector:Injector, protected dispatcher:Dispatcher<any>, handlersProvidersArray:Provider[]) {
this.injector = ReflectiveInjector.resolveAndCreate(handlersProvidersArray, injector);
this.handlers = this.$setHandlers(handlersProvidersArray);
}
private $setHandlers(providersArray:Provider[]):Array<any> {
return providersArray.map((handlerProvider) => this.injector.resolveAndInstantiate(handlerProvider)[0]);
}
protected $handlersToDispatch(action:Action):void {
let handlersActingOnAction = this.handlers
.map(handler => handler.handleCommand(action))
.filter(v => typeof v !== 'undefined');
if (handlersActingOnAction.length) {
handlersActingOnAction.forEach(thunk => this.dispatcher.dispatch(thunk));
} else {
this.dispatcher.dispatch(action);
}
}
}
import {ribbonActions} from "../actions/ribbon.actions";
import {Dispatcher, Action} from "@ngrx/store";
import {Injectable} from "@angular/core";
import {Observable} from "rxjs/Observable";
import {just} from "../../../common/helpers/observable.helpers";
import {transformButtonToUiCommandModel} from "../models/button.model";
import {ServerApi} from "../../../common/services/server.api";
import {IHandler} from "../../../common/models/handler.interface";
@Injectable()
export class RibbonButtonClickHandler implements IHandler {
constructor(private dispatcher:Dispatcher<Action>, private serverApi:ServerApi) {
}
handleCommand(action:Action) {
if (action.type === ribbonActions.RIBBON_BUTTON_CLICKED) {
return (dispatch, getState) => {
dispatch(action);
this.passToServer(action)
.map(response => ({type:(action.payload.sentToServer) ?
ribbonActions.RIBBON_BUTTON_CLICK_SERVER_RESPONSE : ribbonActions.RIBBON_BUTTON_CLICK_CLIENT_HANDLED,
payload: Object.assign({}, action.payload, {response})
}))
.catch(err => Observable.empty())
.subscribe(resAction => dispatch(resAction));
};
}
}
private passToServer(action) {
if (typeof action.payload.button.clientActionOnly === 'undefined') {
action.payload.sentToServer = true;
return this.serverApi.sendCommand('ui', transformButtonToUiCommandModel(action.payload.button))
.catch(e => {
this.dispatcher.dispatch({
type : ribbonActions.RIBBON_BUTTON_CLICK_NOT_HANDLED,
payload: action.payload
});
return Observable.throw(e);
});
} else {
return just(null);
}
}
}
import * as Immutable from "immutable";
import {Action, Reducer} from "@ngrx/store";
import {ribbonActions} from "../actions/ribbon.actions";
import {ribbonInitialState} from "./ribbon.initialstate";
import {RibbonState} from "./ribbon.initialstate";
import {ButtonModel} from "../models/button.model";
import {ActionType} from "../models/button-defs.ts";
export function RibbonReducer(state = ribbonInitialState, action:Action) {
let newState;
switch (action.type) {
case ribbonActions.RIBBON_BUTTON_CLICKED:
newState = state.withMutations(mState => {
let button:ButtonModel = mState.getIn(['buttons', action.payload.button.id]);
button.disabled = true;
button.classList = button.classList.add('active');
});
break;
case ribbonActions.RIBBON_BUTTON_CLICK_SERVER_RESPONSE:
case ribbonActions.RIBBON_BUTTON_CLICK_CLIENT_HANDLED:
case ribbonActions.RIBBON_BUTTON_CLICK_NOT_HANDLED:
newState = state.withMutations(mState => {
let button:ButtonModel = mState.getIn(['buttons', action.payload.button.id]);
button.disabled = false;
button.classList = button.classList.remove('active');
});
break;
default:
newState = state;
}
return newState;
}
Questions? Comments?
ns@nadavsinai.com
twitter:@nadavsinai
Angular 2 in Action
By Nadav SInai
Angular 2 in Action
Solving real life challenges with Angular 2. A short look at the development of a cutting edge medical imaging application @Algotec
- 1,615