AngularJS


Advanced topics








Hugo Josefson / Jayway 2014

Overview

 
  • Component types
  • Structures
     
  • Sharing code, components
  • REST, Hypermedia (HATEOAS)
     
  • References, Useful tools, Questions!
!

The Angular Way

 
  • Data models are POJO  ← this is what you update
    • the data of your app / single source of truth
       
  • Declarative View
    • DOM-based (HTML)
    • two-way data-bound to Model
       
  • Service
    • business-logic
    • works with data

The Angular Way


  • Controller
    • supplies Data models, Services to View

  • Filters (helper functions for Views)
    • filter lists
    • translate values → relevant strings

  • Directive
    • custom UI Behavior
    • custom HTML tags ("extends HTML")
    • manipulates the DOM 

COMPONENT TYPE

Get the best from each

Service
Controller
Directive

Service
Controller
Directive

Dependecy Injection (DI)

A design pattern

Example without DI

function User() {
    this.sessionStore = new SessionStore();
}

var user = new User();

User constructor
controls session store impl

Example with DI

Poor person's DI

function User(sessionStore) {
    this.sessionStore = sessionStore;
}

var user = new User(new SessionStore());

Inversion of Control (IoC)

AngularJS DI

myModule

    .controller('MyController', function($http) {
        $http.get('https://google.com');
    });

Inject services by
function argument name!

Inject built-in AngularJS service

AngularJS DI

myModule

    .value('goodUrls', { google: 'https://google.com' })

    .controller('MyController', function($http, goodUrls) {
        $http.get(goodUrls.google);
    });

Inject services by
function argument name!

Inject own service

Provider recipes

Value recipe

myModule

    .value('googleDotCom', 'https://google.com')

    .value('goodUrls', { google: 'https://google.com' })

    .value('googleUrlGetter', function () {
        return 'https://google.com';
    })

    .controller('MyController', function ($http, googleDotCom, goodUrls, googleUrlGetter) {
        $http.get(googleDotCom);
        $http.get(goodUrls.google);
        $http.get(googleUrlGetter());
    });

Factory recipe

myModule

    .value('googleDotCom', 'https://google.com')

    .factory('goodUrls', function (googleDotCom) {
        return { google: googleDotCom };
    })

    .factory('googleUrlGetter', function (googleDotCom) {
        return function () {
            return googleDotCom;
        };
    })

    .controller('MyController', function ($http, googleDotCom, goodUrls, googleUrlGetter) {
        $http.get(googleDotCom);
        $http.get(goodUrls.google);
        $http.get(googleUrlGetter());
    });

Factory recipe

Service recipe

myModule

    .service('viaConstructor', function GoodUrls() {
        this.google = 'https://google.com';
    })

    .factory('viaFactory', function goodUrlsFactory() {
        return { google: 'https://google.com' };
    })

    .controller('MyController', function ($http, viaFactory, viaConstructor) {
        $http.get(viaConstructor.google);
        $http.get(viaFactory.google);
    });

should be: Constructor recipe

Service recipe

myModule

    .value('googleDotCom', 'https://google.com')

    .service('viaConstructor', function GoodUrls(googleDotCom) {
        this.google = googleDotCom;
    })

    .factory('viaFactory', function goodUrlsFactory(googleDotCom) {
        return { google: googleDotCom };
    })

    .controller('MyController', function ($http, viaFactory, viaConstructor) {
        $http.get(viaConstructor.google);
        $http.get(viaFactory.google);
    });

should be: Constructor recipe

Provider recipe

  • The core recipe
  • Complex and fully-featured
     
  • Value, Factory, Service are shortcuts
     
  • Only use Provider when
    • building reusable service;
      • AND
    • need global configuration by user

Provider recipe

angular.module('searchEngines', [])
    .provider('searchEngine', function SearchEngineProvider () {
        var url = null;
        this.setUrl = function(newUrl) {
            url = newUrl;
        };
        this.$get = function SearchEngineFactory($http) {
            return {
                search: function (query) {
                    $http.get(url + '?q=' + encodeURIComponent(query));
                }
            };
        };
    });


angular.module('myModule', ['searchEngines'])

    .config(function (searchEngineProvider) {
        searchEngineProvider.setUrl('https://google.com');
    })

    .controller('MyController', function (searchEngine) {
        searchEngine.search('cats');
    });

Constant recipe

angular.module('searchEngines', [])
    .provider('searchEngine', function SearchEngineProvider () {
        var url = null;
        this.setUrl = function(newUrl) {
            url = newUrl;
        };
        this.$get = function SearchEngineFactory($http) {
            return {
                search: function (query) {
                    $http.get(url + '?q=' + encodeURIComponent(query));
                }
            };
        };
    });


angular.module('myModule', ['searchEngines'])

    .constant('myUrl', 'https://google.com')

    .config(function (searchEngineProvider, myUrl) {
        searchEngineProvider.setUrl(myUrl);
    })

    .controller('MyController', function (searchEngine) {
        searchEngine.search('cats');
    });

Services summary

How to declare services


.value(name, thing)                        // for non-injectable value, function, object

.factory(name, functionReturningThing)     // for     injectable value, function, object


.service(name, ConstructorForThing)        // not really needed, confusing (injectable)


.provider(name, ConstructorForProvider)    // for user-configurable shared service (injectable)

.constant(name, thing)                     // like .value(), when needed to configure Provider

Service is Singleton

var instances = 0;

myModule

    .factory('myService', function() {
        instances++;
        return { instanceNumber: instances };
    })

    .run(function (myService) {
        console.log('myService instance number: ', myService.instanceNumber);
    })

    .run(function (myService) {
        console.log('myService instance number: ', myService.instanceNumber);
    });
myService instance number: 1
myService instance number: 1

Service
Controller
Directive

Controller

 

  • attached to the DOM via the ng-controller directive
     
  • a JavaScript constructor function that is used to augment the scope
     
  • Angular instantiates a new Controller objects for each ng-controller directive

When to use

 

  • Attach Services to View scope
     
  • Add (non-reusable) behavior to View scope
     
  • Setup, initialize scope objects (models)

When NOT to use

 

Controller is !Singleton

<div ng-controller="MyController as my">{{ my.message }}</div>
<div ng-controller="MyController as my">{{ my.message }}</div>
var instances = 0;
myModule.controller('MyController', function() {
    instances++;
    this.message = 'MyController ' + instances;
});
MyController 1
MyController 2
  • Controllers are re-instantiated on every use
  • Don't trust it to keep state!
  • Use Service for state

Behavior

<div ng-controller="MyController as my">
    <h2>{{ my.employee.company }}</h2>
    
    Employee name: <input ng-model="my.employee.name"/>

    <button ng-click="my.save(my.employee)">Save</button>
</div>
function MyController() {
    this.employee = {
        name: 'Hugo',
        company: 'Jayway'
    };

    this.save = function(person) {
        alert('Save ' + person.name);
    }
}

Behavior

Service
Controller
Directive

What are Directives?

  • Markers on DOM element
    • attribute, element name, css class...
       
  • When AngularJS bootstraps
    1. Normalizes markers
    2. $compiler matches DOM elements to Directives
       
  • Some built-in
    • ngApp, ngView, ngShow, ngDisabled...

Normalization

<span ng-bind="person.name">
<span ng:bind="person.name">
<span ng_bind="person.name">
<span data-ng-bind="person.name">
<span x-ng-bind="person.name">

all normalized to match directive

ngBind

  • Strip x- and data-
  • Convert : - _ to camelCase

Usage Recommendation

  • Use dash-delimited format
    • ng-bind, ng-controller...
       
  • For pre HTML5 validation
    • data-ng-bind, data-ng-controller...
  • Prefer tag name, attribute name
<!-- Prefer: -->
    <my-dir></my-dir>
    <span my-dir="exp"></span>

<!-- Avoid: -->
    <!-- directive: my-dir exp -->
    <span class="my-dir: exp;"></span>

Side note:
The ng-attr-* Directive

<svg>
  <circle cx="{{cx}}"></circle>
</svg>

// Error: Invalid value for attribute cx="{{cx}}"

Avoid incomplete DOM:

<svg>
  <circle ng-attr-cx="{{cx}}"></circle>
</svg>

Use ng-attr-* Directive:

Creating Directives

Template-expanding Directive

myModule

    .directive('jwWorldGreetingHeader', function () {
        return {
            restrict: 'E',
            template: '<h1>Hello, World!!!</h1>'
        };
    });
<jw-world-greeting-header/>
<jw-world-greeting-header>
    <h1>Hello, World!!!</h1>
</jw-world-greeting-header>

    <h1>Hello, World!!!</h1>

Best practice:
use templateUrl

Prefix

Factory function,
returns definition obj

Accept arguments

myModule

    .directive('jwGreetingHeader', function () {
        return {
            restrict: 'E',
            scope: {
                'name': '@'
            },
            template: '<h1>Hello, {{ name }}!!!</h1>'
        };
    });
<jw-greeting-header name="Hugo"/>
<jw-greeting-header name="Hugo" class="ng-isolate-scope">
    <h1 class="ng-binding">Hello, Hugo!!!</h1>
</jw-greeting-header>

    <h1>Hello, Hugo!!!</h1>

isolate scope definition

restrict: 'EAC'

When should I use which?

  • Element (restrict: 'E')
    • Component in control of the template
       
  • Attribute (restrict: 'A')
    • Decorate existing element
       
  • CSS Class (restrict: 'C')
    • Possibly special cases

Transclude

myModule.directive('jwDialog', function () {
    return {
        restrict: 'E',
        transclude: true,
        scope: {
            'title': '@'
        },
        templateUrl: 'jw-dialog.html'
    };
});
<jw-dialog title="Thank you">
    <p>Thank you for registering.</p>
    <p>We'll be in touch.</p>
</jw-dialog>
<jw-dialog title="Thank you" class="ng-isolate-scope">
    <div class="alert alert-success">
        <h4 class="ng-binding">Thank you</h4>
        <ng-transclude>
            <p class="ng-scope">Thank you for registering.</p>
            <p class="ng-scope">We'll be in touch.</p>
        </ng-transclude>
    </div>
</jw-dialog>
<!-- jw-dialog.html -->

<div class="alert alert-success">
    <h4>{{ title }}</h4>
    <ng-transclude/>
</div>
<div class="alert alert-success">
    <h4>Thank you</h4>
    <p>Thank you for registering.</p>
    <p>We'll be in touch.</p>
</div>

Expose API

Expose API from Directive

plnkr.co/edit/0OaCZA

Link function

myModule.directive('jwDialog', function ($interval) {
    return {
        restrict: 'E',
        transclude: true,
        scope: { 'title': '@' },
        templateUrl: 'jw-dialog.html',
        link: function (scope, element, attr) {

            scope.secondsPassed = 0;
            
            var timeoutId = $interval(function() {
                scope.secondsPassed++;
            }, 1000);

            element.on('$destroy', function() {
                $interval.clear(timeoutId);
            });
        }
    };
});
<!-- jw-dialog.html -->

<div class="alert alert-success">
    <h4>{{ title }}</h4>
    <ng-transclude/>
    <small>Visible since {{ secondsPassed }} seconds ago...</small>
</div>

WAYS TO STRUCTURE

an AngularJS app

Component structures

Rules of

The Angular Way

apply

  • Common business logic → Service
  • Data / State → Service
  • View specific behavior → Controller
  • Common view data transformation → Filter
  • Common view behavior → Directive

Module structures

  • An angular app has a main Module
  • Every component belongs to a Module
<html ng-app="myApplication">
    <head>
        <script src="angular.js"></script>
        <script src="my-application.js"></script>
    ...
</html>
// my-application.js

// Create module
angular.module('myApplication', []);

// Get module
var mod = angular.module('myApplication');

// Attach recipes for Service, Directive, Controller etc...
mod.value('myService', {some: 'object'});

← Different syntax!

← Different syntax!

Module dependencies

Uselessness of Modules

  • Each component belongs to a Module
  • Services are injected via service name
angular.module('myApplication', [])

    .value('myService', {some: 'thing'}

    .controller('MyController', function(myService) {
        alert(myService.some); // -> 'thing'
    });

BUT!

Uselessness of Modules

  • Service names share namespace
    even when from different Module
angular.module('myOtherModule', [])
    .value('myService', {some: 'thing'});

angular.module('myApplication', ['myOtherModule'])
    .value('myService', {what: 'ever'}); // -> SILENT name collision!

Uselessness of Modules

Workarounds

  • Don't split an app in Modules
    • Usually no benefit
       
  • Only extract a Module
    • when extracting reusable component
    • and use prefixes (service names)

File and Directory

Structures

  • Group files by feature, not file ext.
    • menu/, greeter/, todo/
  • modules/ directory
  • Use build tool
    • Gulp
    • Grunt

File and Directory

Structures

Build scripts

Purpose

  • DRY code and configuration
    • generate from single source
    • precompile HTML templates → JS
       
  • Optimize deliverable (size, speed)
    • minify, concatenate, optimize img's...
       
  • Organize code YOUR way

Build scripts

Grunt

  1. Read source file from disk
  2. ​→ write file1 to disk
     
  3. ​Read file1 from disk
  4. ​→ write file2 to disk
     
  5. ​Read file2 from disk
  6. ​→ write destination file to disk

Gulp

  1. Read source file from disk
    ​→ stream in RAM
    ​→ stream in RAM
  2. → write destination file to disk

"the JavaScript task runner"

"the streaming build system"

  • Code over Configuration
     
  • File I/O operations:
  • Configuration over Code
     
  • File I/O operations:

SHARING

code / components

Make your components play well with others

  • Clear API
     
  • No globals
    (especially no global state)
     
  • Make possible:
    use twice on same page

Share code
with AngularJS projects

  • One Module
     
  • Directive(s)
    • Prefix names
    • Params: model, fn call
       
  • Service(s)
    • Prefix names
    • (Provider for customization)
  • README.md
    • What it is
    • How to install
    • API
  • bower.json

Share code

with non-AngularJS projects

Vanilla.js

  • No external dependencies
  • Useful without any framework
  • Expose with bower.json + package.json
  • Write README.md
    • What it is
    • How to install
    • API
    • (example use from AngularJS, Knockout, ...)

References

REST / HYPERMEDIA

(HATEOAS)

What is REST?

  • REST
  • Mandatory constraint (blog) HATEOAS
    • Hypermedia as the
      Engine of Application State
    • Link from one state to next,
      followed by the client
      without prior knowledge of URL

The Web, for machines

Non-HATEOAS REST-ish API

Fetch all TODO items

    GET https://server.domain.tld/api/todos


Create new TODO item

    POST https://server.domain.tld/api/todos
        title:       string, maxLength 256, required
        description: string, maxLength 65536
        done:        boolean


Fetch single TODO item

    GET https://server.domain.tld/api/todos/<id>


Update single TODO item

    PUT https://server.domain.tld/api/todos/<id>
        title:       string, maxLength 256, required
        description: string, maxLength 65536
        done:        boolean


Delete single TODO item

    DELETE https://server.domain.tld/api/todos/<id>

REST without HATEOAS?

  • Then it's not REST
     
  • HTTP-based API
  • RPC over HTTP
  • Event messages over HTTP
  • "Bunch of hard-coded URLs,
    requiring lots of documentation"

REST with Hypermedia

EntryPoint

    URL https://server.domain.tld/api


Rels to be aware of

    create Creates a new item
    update Saves a new version of item
    delete Deletes an item

    todos  Returns a collection of TODO items


Expect

   links in "_links",
   JSON_Schema in "_links.<rel>.schema",
   collection items in "_items"

Hypermedia Client

  • Only known URL is Entry Point
     
  • Understands how resources can be linked
     
  • Doesn't assume all functionality is there, always
     
  • UI dynamically based on API

Hypermedia Client

  • Minimal knowledge of server structure
     
  • Tolerant to server changes
     
  • UI obeys server API

Example non-Hypermedia client
in AngularJS

Example Hypermedia client
in AngularJS

Useful tools

AngularJS Batarang Chrome extension
plnkr.cojsfiddle.net Sketch some code
bower.io Dependency management
gulpjs.comgruntjs.com Build systems
yeoman.io Project scaffolding (many Generators)
gulp-example-project, ng-boilerplate, angular-app, generator-gulp-ng Per-feature directory structure examples
ng-annotate Don't worry about coding minifier-safely
ngmodules.org Some Angular community modules

Thank you!

 
 
 
 
Questions!?
 
 
 
 
 
hugo.josefson@jayway.com
Made with Slides.com