Component & Model pattern

for

Angular JS

AUTHOR

TOMAS HERICH

Software Engineer Frontend / Java @mimacom

 

         @tomastrajan

         github.com/tomastrajan

         medium.com/@tomastrajan

 

  • more than two years of Angular JS experience
  • projects of various sizes and in various domains
  • Angular JS focused blog
  • curious affinity for frontend build systems :)
  • reddit /r/javascript /r/programming /r/angularjs addict

 

LET'S GET STARTED

ORIGINAL BLOG POSTS @medium

OVERVIEw

Component Pattern

  • modeling views as a component tree 
  • component pattern, how to implement it in Angular 1.X
  • using ui-router to navigate to particular component
  • reusing components
  • looking forward - how to migrate to Angular 2.0

 

  • introduction to ES6 syntax
  • evolution of concepts (ng-controller, ngRouter, ui-router, ...)

 

OVErview

MODEL Pattern

  • no model ($scope, $watch, data in controller)
  • the "protomodel" (data in service)
  • modeling data as standardized reusable models
  • using ui-router to initialize models 
  • resolving dependencies between models
  • models as single source of truth
  • namespaced models

 

  • that's all folks! conclusion, presentation link

INTRODUCTION TO ES6 syntax

// module syntax
import * as _ from 'lodash';

// classes
class MyUsefulComponent {
    constructor(someDataModel) {
        this.someDataModel = someDataModel;
    }
    
    calculate(params) {
        someDataModel.performSomeComplexCalculation(params);
    }
}

// let, const, arrow functions ...
let x = 3;
const obj = {x};
((obj) => {
    console.log(obj.x); // 3
})(obj);
  • module syntax
  • class syntax
  • const, let, arrow functions, enhanced object literal
  • check more @BABEL transpiler documentation

EVOLUTION OF CONCEPTS

ROAD FROM FROM ANCIENT ng-CONTROLLER to COMPONENTS

COMPONENT PATTERN

EVOLUTION OF CONCEPTS

ROAD FROM FROM ANCIENT ng-CONTROLLER to COMPONENTS

COMPONENT PATTERN

  • ng-controller directly in HTML template
  • $scope (& scope inheritance and resulting problems)
  • ngRoute - better but not flexible (eg: no nested states / views)
  • controllerAs syntax - solution to $scope problems (Angular 1.2+)
  • ui-router - solution to ngRoute problems (nested states, partial views)
  • bindToController syntax for implementing directives without $scope

 

  • components - changing of paradigm (web components, polymer re-usability)
  • component pattern for Angular 1.X
  • ngNewRouter - easier routing to components (convention over conf...)
  • Angular 2.0 first class components

CURRENT STATE OF ART AND FUTURE

<<< IN THIS PRESENTATION !

COMPONENT PATTERN

NG-CONTROLLER

IN TEMPLATE...

<div ng-app="myApp" ng-controller="myCtrl">

    First Name: <input type="text" ng-model="firstName"><br>
    Last Name: <input type="text" ng-model="lastName"><br>
    
    Full Name: {{firstName + " " + lastName}}

</div>

// myCtrl.js
angular.module('myApp', [])
    controller('myCtrl', function($scope) {
        $scope.firstName = "John";
        $scope.lastName = "Doe";
    });

  • only for very simple applications, no routing

COMPONENT PATTERN

$scope

AND NESTED SCOPE INHERITANCE PROBLEMS...

<div ng-app="myApp" ng-controller="parentCtrl">

    Parent: <input type="text" ng-model="user.name" />

    <div ng-controller="childCtrl">
        Child: <input type="text" ng-model="user.name" />
    </div>

</div>

// myCtrl.js
angular.module('myApp', [])
    .controller('parentCtrl', function($scope) {
        $scope.user = { 
            name: 'John Doe'
        }
    })
    .controller('childCtrl', function($scope) {
    });
  • scope inheritance, tight coupling, problems with debugging and reusability

COMPONENT PATTERN

$scope

AND NESTED SCOPE INHERITANCE PROBLEMS LIVE EXAMPLE...

COMPONENT PATTERN

$scope

AND NESTED SCOPE INHERITANCE PROBLEMS LIVE EXAMPLE...

  • live examples are great, mainly if you have internet connection

COMPONENT PATTERN

ng Route

official but not that great routing solution

angular.module('app', []);
  
    .config(['$routeProvider', function($routeProvider) {
        $routeProvider
            .when('/someUrl', {
                templateUrl: 'some-tempalte.html',
                controller: 'SomeController',
                resolve: {}
             });
  • decouple controller from template
  • support for resolving data before entering controller
  • not flexible enough
  • impossible to nest states and views (only top level ng-view)

COMPONENT PATTERN

controller as syntax

SOLution to $scope inheritance problem

// routes.js
angular.module('app', [])
    .config(['$routeProvider', function($routeProvider) {
        $routeProvider
            .when('/someUrl', {
                templateUrl: 'some-tempalte.html',
                controller: 'SomeController as ctrl'
             });

// someController.js
angular.module('app')
    .controller('SomeController', function() {
        this.property = 'some value';
    });

// some-tempalte.html
<div>
    {{ctrl.property}}
</div>

  • properties bound directly to controller (controller's this)
  • removed dependency to $scope
  • enhanced re-usability

COMPONENT PATTERN

UI-ROUTER

THE BEGINNING OF THE GREAT ERA

$stateProvider
    .state('parent', {
        url: '/parent',
        templateUrl: 'parent.html'
    })
    .state('parent.child', {
        url: '/child',
        templateUrl: 'child.html'
    })
    .state('parent.anotherchild', {
        url: '/anotherchild',
        views: {
            '': { templateUrl: 'anotherchild.html' },
            'sidebar@anotherchild': { // ui-view="sidebar"
                template: 'Look I am a sidebar!' 
            } 
        }
    });
  • routing based on unique state name not url
  • nested states injected into ui-view directive in parent template
  • partial views

COMPONENT PATTERN

BIND TO CONTROLLER

SYNTAX THAT ENABLES CONTROLLER AS USAGE FOR DIRECTIVES WITH ISOLATE SCOPE

// some app tempalte
<div some-directive prop='value'></div>

// directive
angular.module('app', [])
    .directive('someDirective', function () {
        return {
            scope: {
                prop: '@'
            },
            templateUrl: 'someTemplate.html',
            controller: SomeController,
            controllerAs: 'ctrl',
            bindToController: true // also as of angular 1.4.1 it is possible 
                                   // to specify props {} here instead of scope   
        };
    })
    .controller('SomeController', function() {
        this.prop; // 'value'
    });

// directive tempalte: someTemplate.html
<div>{{ctrl.prop}}</div>

  • bind scope properties to controller's this automatically

COMPONENT PATTERN

COMPONENTS

THE PARADIGM SHIFT

The component model for the Web (also known as Web     Components) consists of pieces designed to be used together to let web application authors define widgets with a level of visual richness not possible with CSS alone, and ease of composition and reuse not possible with script libraries today

<bootstrap-navbar sticky></bootstrap-navbar>
<section class="container" role="main">  
  <!-- ... -->
</section>   
<bootstrap-footer></bootstrap-footer> 

COMPONENT PATTERN

COMPONENT PATTERN

FOR ANGULAR 1.X

angular
    .module('app', [])
    .directive('someComponent', someComponent);

    function someComponent() {
        return {
            restrict: 'A',
            scope: {
                // isolated scope, use to pass data from parent, eg: data: '='
            },
            controller: SomeComponent,
            controllerAs: 'ctrl',
            bindToController: true,
            templateUrl: 'some-component.tpl.html'
        };
    }

    function SomeComponent(SomeService, SomeOtherDependency) {
        this.name = 'John Doe';
    }
  • directive used to define component (controller & template)
  • encapsulated functionality, dependencies are passed explicitly

COMPONENT PATTERN

new ng router

convention over directive definition object

// app.js
angular.module('app', ['ngNewRouter'])
    .controller('AppController', ['$router', AppController]);

AppController.$routeConfig([
    {path: '/', component: 'home' }
]);
function AppController ($router) {}

// components/home/home.js
angular.module('app.home', [])
    .controller('HomeController', [function () {
        this.name = 'John Doe';
    }]);

// components/home/home.html
<h1>Hello {{home.name}}!</h1>
  • component name is specified in route definition

COMPONENT PATTERN

ANGULAR 2.0

FIRST CLASS COMPONENTS

  • component and template specified by decorators (@Component & @Template)
@Component({
    selector: 'some-component',
    componentServices: [
        SomeService
    ]
})
@Template({
    url: './some-component.html',
    directives: [Foreach] // directives used in template
})
class SomeComponent {
    constructor() {
        this.SomeService = SomeService;
        this.name= 'John Doe';
    }
    doStuff() {
        this.SomeService.doStuff();
    }
}

COMPONENT PATTERN

OK... LET'S DO THIS

COMPONENT PATTERN

MODELING VIEWS

AS A COMPONENT TREE

COMPONENT PATTERN

COMPONENT PATTERN

FOR ANGULAR 1.X - DIRECTIVE DEFINITION OBJECT

  • directive used to define component (controller & template)
// someComponent.js

// directive definition object used to specify component

function someComponent() {
    return {
        restrict: 'A', // only attribute eg: <div my-component></div>
        scope: {
            // isolated scope, use to pass data from parent, eg: data: '='
        },
        controller: SomeComponent,             // controller function
        controllerAs: 'ctrl',                  // controller alias in template
        bindToController: true,                // bind scope props to controller's this
        templateUrl: 'some-component.tpl.html' // components template url
    };
}

COMPONENT PATTERN

COMPONENT PATTERN

FOR ANGULAR 1.X - COMPONENT'S CONTROLLER

  • component's controller referenced in previously specified directive definition object
  • functionality for component's template
// someComponent.js

// @ngInject
function SomeComponent(SomeService) {
    this.name = 'John Doe';

    this.calculate= calculate;

    function calculate(param) {
        return SomeService.performCalculation(param);
    }
}

COMPONENT PATTERN

COMPONENT PATTERN

FOR ANGULAR 1.X - COMPONENT'S TEMPLATE

  • show data and call functions exposed in component's controller
  • only data and actions of the component's controller because of isolation (directive's isolated scope and controllerAs syntax)
// some-component.tpl.html

<div>
    {{ctrl.name}}

    <button ng-click="ctrl.calculate();">Calculate!</button>
</div>

COMPONENT PATTERN

COMPONENT PATTERN

NAVIGATE TO PARTICULAR COMPONENT WITH UI-ROUTER

  • specify component as a inline template of the state
// some-component.tpl.html

angular
    .module('app', ['ui.router'])
    .config(config);

    // @ngInject
    function config($stateProvider) {
        $stateProvider
            .state('app.some', {
                url: '/some',
                template: '<div some-component></div>',
                resolve: {
                    // ... resolve data, init models
                },
            });
    }

COMPONENT PATTERN

COMPONENT PATTERN

REUSE EXISTING COMPONENTS

  • reuse component in different states
  • reuse component in template of other component
// for multiple routes
$stateProvider
    .state('app.admin.users.user', {
        url: '/app/admin/users/user/{userId}" ',
        template: '<div user-info-component></div>'
    })
    // ... other states ...
    .state('app.user.profile', {
        url: '/app/user/{userId}/profile" ',
        template: '<div user-info-component></div>'
    });

// inside of another component's template... header.tpl.html
<div>
    <!-- navigation ... -->
    <div user-info-component></div>
<div>

COMPONENT PATTERN

COMPONENT PATTERN

EASY MIGRATION TO ANGULAR 2.0 

  • use component together with ES6 syntax for extra convenient migration to Angular 2.0
  • use newNgRouter as soon as available to get rid of directive definition object
  • use ES7/TS @decorators when Angular 2.0 is available
// someComponent.js (ES6)
class SomeComponent {
    constructor(SomeService) {
        this.SomeService = SomeService;
    }
 
    calculate() {
        this.SomeService.performCalculation();
    }
}

YES !

WE'RE HALF WAY THROUGH ! 

 

  • we understand evolution of Angular JS concepts
  • we know what components are
  • we know how to implement them using component pattern
  • we hope we're prepared for the future !

LET'S MODEL DATA

ANCIENT ANGULAR AKA EVERY BASIC ANGULAR TUTORIAL

  • data in controller's $scope
  • problem with synchronization ($watch, events)
  • no separation of concerns

MODEL PATTERN

angular.module('app', [])
    .controller('MyOvercompetentController', function($http) {

        init();

        function init() {
            $http.get('/api/my/data/endpoint').then(function(response) {
                $scope.data = response.data;
            });
        }

        $scope.add = function(newObject) {
            $scope.data.push(newObject);
        };
    });

THE PROTOMODEL

DATA IN SERVICE

  • use and store $http response in service, expose data in controller

MODEL PATTERN

function SomeService($http) {
    var data = [];
    return {
        data: data,
        findAll: findAll 
    }
    function findAll() {
        return $http.get('/some').then(function(response) {
            angular.copy(response.data, data); // angular.copy to preserve reference
        });
    }
 
}
 
function SomeController(SomeService) {
    var ctrl = this;
    ctrl.data = SomeService.data; // bind model to controller (create reference)
}
 
<div ng-repeat="d in ctrl.data">
     {{d.property}}
</div>

MODEL PATTERN

THE MODEL - MODELING DATA AS STANDARDIZED REUSABLE MODELS (ES6)

MODEL PATTERN

class BookModel {
 
    constructor($q, BookRestResource) {
        this.BookRestResource = BookRestResource;
   
        this.collection = [];
        this.item = {};
    }
 
    initCollection() {
        return this.BookRestResource.findAll().then((books) => {
            
            /* 
                response.data is unwrapped centrally in   
                httpInterceptor for every successful request
            */
            this.collection = books;
 
        });
    }
 
    initItem(bookId) { 
 
        // init item from backend
        if (bookId) {
            return this.BookRestResource.findById(bookId)
                .then((book) => {
                    this.item = book;
                });
 
        // init new item 
        } else {
            this.item = { 
                // init some default values if necessary eg:
                property: 'defaultValue'
            };
 
            /*
                we return and resolve helper promise to assure
                consistent API of method so that we can always
                use .then() method when calling initItem
            */
            return $q.resolve();
        }
    }
 
    save() {
        /*
            update existing item if model contains id 
            (could contain more complex checking logic based
             model's specific needs)
        */
        if (this.item.id) {
            return this.BookRestResource.update(this.item);
        } else {
            /*
               we use CQRS approach in backend
               (no side effects for GET and no return
               value for POST, PUT, DELETE) so we have 
               to refresh model manually when needed
               
               only exception to this rule is returning id 
               when creating new entity so that we can 
               refresh item model with newly created item
            */    
            return this.BookRestResource.create(this.item)
                .then((bookId) => { 
                    return bookId;
                });
        }
    }
 
    // example of business method
    filterByAuthorName(authorName) {
        return _.filter(this.collection, (book) => {
             return book.authorName === authorName;
        });  
    }
 
}

MODEL PATTERN

INITIALIZE MODELS IN UI-ROUTER & RESOLVE DEPENDENCIES BETWEEN MODELS

MODEL PATTERN

$stateProvider
    .state('app.offer.item', {
        url: '/item/:itemId',
        template: '<div item-component></div>',
        resolve: {
            // doesn't return data, just initialize model which are used by components
            init: function($stateParams, ItemModel, DiscountModel) {
                // parallel / no dependency
                return $q.all([
                    DiscountModel.initCollection(),
                    ItemModel.initItem($stateParams.itemId)
                ]);

                // ... or serial / dependency between models
                return ItemModel.initItem($stateParams.itemId)
                    .then(function(item) {
                        return DiscountModel.initCollection(item.category);
                    });
            }
        }
    });

MODEL PATTERN

USE MODELS IN COMPONENTS (CONTROLLER & TEMPLATE)

MODEL PATTERN

// component's controller
class ItemDetailComponent {
    constructor(ItemModel, DiscountModel) {
        this.ItemModel = ItemModel;
        this.DiscountModel = DiscountModel;
    }

    calculate() {
        return this.ItemModel.doSomeBusinessProcessing();
    }
}

// component's tempalte
<div>
    Product: {{ctrl.ItemModel.item.name}} <br />
    Price: {{ctrl.ItemModel.item.price}}
</div>
<div ng-repeat="discount in ctrl.DiscountModel.collection">
     <p>{{discount.info}}</p>
</div>
<button ng-click="calculate()">Calculate</button>

MODEL PATTERN

THE SINGLE SOURCE OF TRUTH

  • sharing one model in multiple components
  • components display same data in a different way
  • solved automatically by using models
  • inject model into all interested components
  • automatically display correctly synchronized data

MODEL PATTERN

MODEL PATTERN

BONUS: NAMESPACED MODELS

MODEL PATTERN

class NameSpacedModel {
 
    constructor($q, SomeRestResource) {
        this.SomeRestResource = SomeRestResource;
   
        this.models = {
            main: {
                collection: [],
                item: {}
            }
        };
    }
     
    // convenience getters for accessing main model directly
    get collection() {
        return this.models.main.collection;
    }
 
    get item() {
        return this.models.main.item;
    }
 
    initCollection(modelId) {
        return this.SomeRestResource.findAll().then((data) => {
            return this.getModel(modelId).collection = data;
        });
    }
 
    // helper method for accessing desired model namespace
    getModel(modelId = 'main') {
        this.models[modelId] = this.models[modelId] ?  
            this.models[modelId] : 
            { collection: [], item: {} };
        return this.models[modelId];
    }
 
}

THAT'S ALL FOLKS !

I HOPE YOU ENJOYED THE PRESENTATION

https://slides.com/tomastrajan/component-and-model-pattern-for-angular-js

PRESENTATION LINK

  • about evolution of Angular JS concepts
  • how to use them to implement component pattern
  • why it makes sense and how it helps with respect to future
  • how to model data
  • how to implement models 
  • how it helps to make app state easier to synchronize and easier to reason about

WE LEARNED

THE END !

THANK YOU FOR YOUR ATTENTION AND DON'T FORGET TO...

TONIGHT !

Component & Model pattern for Angular JS

By Tomáš Trajan

Component & Model pattern for Angular JS

Component & Model pattern for Angular JS 1.4

  • 3,555