Solid angular

Preview:

Citation preview

HOW ANGULAR CAN SOLVE ALL YOUR PROBLEMS

Nir Kaufman

IT CAN’T

Nir Kaufman

- I don’t really need glasses to see - This photo is a photoshop - In reality I’m in color (and fatter)

Head of AngularJS Development @ 500Tech

INTRODUCTION

“We are not happy with our app. it should be modular, easy to extend and maintain. It’s hard to understand the flow, feels like a spaghetti of presentation and business logic.

- frontend team at {{ company.name }}

WE NEED A BETTER FRAMEWORK

WHAT DO WE NEED?

COMPONENTS

A clean way of organizing your UI code into self-contained, reusable chunks

// component controller class likeBoxController { constructor(params) { this.chosenValue = params.value; } like() { this.chosenValue('like'); } dislike() { this.chosenValue('dislike'); } } // component definition function likeBoxComponent() { return { viewModel: likeBoxController, template: likeBoxTemplate } } // component registration ko.components.register('like-widget', likeBoxComponent());

// component controller class likeBoxController { constructor() { this.chosenValue = null; } like() { this.chosenValue = 'like'; } dislike() { this.chosenValue = 'dislike'; } } // component definition function likeBoxComponent() { return { controller: likeBoxController, scope: { params: ‘=chosenValue' }, controllerAs: 'LikeBox', bindToController: true, template: likeBoxTemplate } } angular.module('app', []) .directive('likeWidget', likeBoxComponent);

<div class="like-or-dislike" data-bind="visible: !chosenValue()"> <button data-bind="click: like">Like it</button> <button data-bind="click: dislike">Dislike it</button> </div> <div class="result" data-bind="visible: chosenValue"> You <strong data-bind="text: chosenValue"></strong> it </div>

<div class="like-or-dislike" ng-hide="LikeBox.chosenValue"> <button ng-click="LikeBox.like()">Like it</button> <button ng-click="LikeBox.dislike()">Dislike it</button> </div> <div class="result" ng-show="LikeBox.chosenValue"> You <strong ng-bind="LikeBox.chosenValue"></strong> it </div>

class LikeWidget extends React.Component { constructor(props) { super(props); this.state = { chosenValue: null }; this.like = this.like.bind(this); this.dislike = this.dislike.bind(this); this._likeButtons = this._likeButtons.bind(this) } like() { this.setState({ chosenValue: 'like' }) } dislike() { this.setState({ chosenValue: 'dislike' }) } _likeButtons() { if (this.state.chosenValue) { return null } return ( <div> <button onClick={this.like}>Like it</button> <button onClick={this.dislike}>Dislike it</button> </div> ) } render() { return ( <div> { this._likeButtons() } { this.state.chosenValue ? <div>You <strong>{ this.state.chosenValue }</strong> it </div> : null} </div> ) } } React.render(<LikeWidget/>, document.getElementById('app'));

MVW PATTERN

Keep your business logic separate from your user interface

class MyViewModel { constructor() { this.products = [ new Product('Garlic bread'), new Product('Pain au chocolat'), new Product('Seagull spaghetti', 'like') ]; } } ko.applyBindings(new MyViewModel());

class MyViewModel { constructor() { this.products = [ new Product('Garlic bread'), new Product('Pain au chocolat'), new Product('Seagull spaghetti', 'like') ]; } } angular.module('app', []) .controller('MyViewModel', MyViewModel);

Backbone.Model.extend({ defaults: { coverImage: 'img/placeholder.png', title: 'No title', author: 'Unknown', releaseDate: 'Unknown', keywords: 'None' } });

DS.Model.extend({ title: DS.attr('No title'), author: DS.attr('Unknown'), releaseDate: DS.attr('Unknown'), keywords: DS.attr('None') });

class Book { constructor() { this.coverImage = 'img/placeholder.png'; this.title = 'No title'; this.author = 'Unknown'; this.releaseDate = 'Unknown'; this.keywords = 'None'; } }

LETS GET TO THE POINT

All major frameworks introduce the same concepts.

Don’t make a switch for the wrong reasons. Switching to another framework won’t solve your design problems.

OBJECT ORIENTED PROGRAMMING

CONSIDER TYPESCRIPT

I used to hate it…

SOLID PRINCIPLESSingle Responsibility

Open / Closed

Liskov Substitution

Interface Segregation

Dependency Inversion

S.O.L.I.DSingle Responsibility Principle

A module should have one, and only one reason to change

// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(($stateProvider, $httpProvider, localStorageServiceProvider) => { // start routing $stateProvider .state('dashboard', { url: '/dashboard', templateUrl: 'states/dashboard/dashboard.html', controller: 'DashboardController' }) .state('users', { url: '/users', templateUrl: 'states/users/users.html', controller: 'UsersController' }); // http configuration $httpProvider.useApplyAsync(true); $httpProvider.useLegacyPromiseExtensions(false); $httpProvider.interceptors.push(($log) => { return { 'request': function (config) { $log.debug(config); return config; } }; }); // storage configurations localStorageServiceProvider .setPrefix('myApp') .setStorageType('sessionStorage') }); // start engines angular.bootstrap(document, ['app']);

4 reasons to change this module:

add dependency add new state configure http service configure storage service

// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(routes) .config(http) .config(storage) // start engines angular.bootstrap(document, ['app']);

export function routes($stateProvider) { $stateProvider .state('dashboard', { url: '/dashboard', templateUrl: 'states/dashboard/dashboard.html', controller: 'DashboardController' }) .state('users', { url: '/users', templateUrl: 'states/users/users.html', controller: 'UsersController' }); }

routes.ts

app.ts

export function http ($httpProvider) { $httpProvider.useApplyAsync(true); $httpProvider.useLegacyPromiseExtensions(false); }

http.ts

export function storage(localStorageServiceProvider) { localStorageServiceProvider .setPrefix('myApp') .setStorageType('sessionStorage') }

storage.ts

// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) .config(routes) .config(http) .config(storage) // start engines angular.bootstrap(document, ['app']);

Are we there yet?

2 reasons to change

// module declarations angular.module('app', [ 'ui.router', 'LocalStorageModule', 'app.states' ]) // start engines angular.bootstrap(document, ['app']);

1 responsibility

S.O.L.I.DOpen / Closed Principle

A module should be open for extension, but closed for modification.

class Modal { constructor($modal) { this.modal = $modal; } show(type) { switch (type) { case 'login': this.showLoginModal(); break; case 'info': this.showInfoModal(); break; } } showLoginModal() { this.modal.open({ template: 'loginModal.html', controller: ‘loginModalController’ }) } showInfoModal() { this.modal.open({ template: 'infoModal.html', controller: 'infoModalController' }) } }

class Controller { constructor(Modal) { this.Modal = Modal; } showLogin(){ this.Modal.show('login'); } }

We need to add new Modals to our system

class Modal { constructor($modal) { this.modal = $modal; this.modals = new Map(); } register(type, config) { this.modals.set(type, config) } $get() { return { show: this.show } } show(type) { if(this.modals.has(type)){ this.modal.open(this.modals.get(type)) } } }

angular.module('app', []) .config(ModalProvider => { ModalProvider.register('lostPassword', { template: 'lostPassword.html', controller: 'lostPasswordController' }) });

class Controller { constructor(Modal) { this.Modal = Modal; } showLogin(){ this.Modal.show('lostPassword'); } }

Write code that can be extended

S.O.L.I.DLiskov Substitution Principle

Child classes should never break the parent class type definitions

IT’S ABOUT INHERITANCE

DON’T DO IT

S.O.L.I.DInterface Segregation Principle

Many specific interfaces are better than one generic interface

I just want to make a copy

class LocalStorage { private storage; private $window; constructor($window, $q) { this.$window = $window; } setStorageType(type:string) { if (type === 'local') { return this.storage = this.$window.localStorage; } if (type === 'db') { return this.storage = new PouchDB('DB'); } } setLocalItem(key:string, data) { if (this.db) { return this.db.put(JSON.parse(data)) } return this.storage.setItem(key, JSON.stringify(data)); }

put(data) { this.storage.put(JSON.parse(data)) }

getLocalItem(key:string):string { let deferred = this.$q.defer(); if (this.db) { this.db.get(key).then( result => deferred.resolve() ) } deferred.resolve(this.storage.getItem()); return deferred.promise; } }

No client should be forced to depend on methods it doesn’t use

class LocalStorage { private storage; constructor($window) { this.storage = $window.localStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }

class SessionStorage { private storage; constructor($window, $q) { this.storage = $window.sessionStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }

localStorage.ts

sessionStorage.ts

class DBStorage { private db; constructor(PouchDB) { this.db = new PouchDB('DB'); } put(data) { this.db.put(data) } get(id){ return this.db.get(id); } }

dbStorage.ts

UserComponent.ts (client)class UserComponent { private storage; constructor(LocalStorage) { this.storage = LocalStorage } }

S.O.L.I.DDependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

NATIVE API’s

ANGULAR

3RD PARTY MODULES

APLLICATION CODE

INTERFACES

Application Layers

YOUR MODULES

Abstraction

Abstraction

interface IStorage { setItem(key:string, data:any); getItem(key:string, data:any); }

IStorgae.ts UserComponent.ts (client)class UserComponent { private storage; constructor(Storage: IStorage) { this.storage = LocalStorage } }

LocalStorage.ts

class LocalStorage implements IStorage { private storage; constructor($window) { this.storage = $window.localStorage; } setItem(key:string, data) { return this.storage.setItem(key, JSON.stringify(data)); } getItem(key:string):string { return this.storage.getItem(); } }

The ‘client’ can work with any kind of storage that implements the IStorage interface

export class WelcomeController { constructor($modal) { this.$modal = $modal; } login() { let loginModalInstance = this.$modal.open({ templateUrl: 'states/welcome/login_modal.html', keyboard: false, backdrop: 'static', controller: LoginModalController, controllerAs: 'Login' }); loginModalInstance.result .then(result => console.log(result)) } }

Angular Bootstrap Modal

export class DashboardController { constructor($modal) { this.$modal = $modal; } showInfo() { let infoModalInstance = this.$modal.open({ templateUrl: 'states/dashboard/info_modal.html', keyboard: false, backdrop: 'static', controller: InfoModalController, controllerAs: 'Info' }); infoModalInstance.result .then(result => console.log(result)) } }

What is the problem?

class Modal { constructor($modal) { this.modal = $modal; this.modals = new Map(); } register(type, config) { this.modals.set(type, config) } $get() { return { show: this.show } } show(type) { if(this.modals.has(type)){ this.modal.open(this.modals.get(type)) } } }

Abstraction without TypeScript

SUMMARY

DON’T MAKE A SWITCH FOR THE WRONG

REASONS

DESIGN PATTENS

MATTER

GOOD DESIGN IS FRAMEWORK

AGNOSTIC

THANK YOU!

Angular ES6 / TypeScript Startershttps://github.com/nirkaufman/angular-webpack-starter

https://github.com/nirkaufman/angular-webpack-typescript-starter

resourceshttps://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

http://code.tutsplus.com/series/the-solid-principles--cms-634

http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

Read our blog:http://blog.500tech.com

Nir Kaufmannir@500tech.com

Recommended