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,425