Angular JS

The FortiOS way

Routes
Components
models

Routes

Routes distinguish sections of a Single Page App

$routeProvider
        .when('/log/view/:logType', {
            template: '<f-log-view></f-log-view>',
            css: [
                'main.css',
                '/ng/directives/faceted_search/faceted_search.css'
            ],
            resolve: loaderProvider.resolve(['f-log-view'])
        })
        .when(...)
        .when(...);
http://fortigate/ng/log/view/app      => $routeParams: {logType: 'app'}
http://fortigate/ng/log/view/ips      => $routeParams: {logType: 'ips'}
http://fortigate/ng/log/view/av       => $routeParams: {logType: 'av'}
http://fortigate/ng/log/view/traffic  => $routeParams: {logType: 'traffic'}

These urls will load the <f-log-view> component and fill in the $routeParams Service

Route Configuration

Use an inline template to load the main route component

template: '<f-log-view></f-log-view>'

Add stylus the output from files using the custom css property

css: [
    'main.css',
    '/ng/directives/faceted_search/faceted_search.css'
]

Use loaderProvider to resolve the component's lazy module.

// only load one requirejs module
resolve: loaderProvider.resolve(['f-log-view'])

Lazy Loading

Lazy Loading

  • preLoader aka require('loader')

  • .preloadModules(['module', ...])

  • Used by $.ng_app()

  • Requires all               modules recursively, so they can be instantiated synchronously when the app runs.

  • .initModules(['module', ...], currentModule)

  • .base_path('template.html', currentModule)

  • .cache_path() .route_path()

 DEPRECATED!

  • Run time loader methods

  • loader aka LoaderService
  • loader.js provides multiple utilities to facilitate lazy loading:

  • loaderProvider

  • Configuration time methods

  • Saves other providers to use later

  • .resolve(['module', ...])

Lazy Modules

Lazy Modules

/ng/log/view/:logType

/ng/log/view/traffic

loaderProvider.resolve(['f-log-view'])
loader.initModules(['f-log-view'])

f-log-view.js

return function register(providers, loader) {
        providers.$compile.component({
            fLogView: fLogView
        });

        return loader.initModules([
            'data',
            'formatters',
            'f-log-view-menu',
            'f-log-details',
            '/ng/directives/menu/templates'
        ], module);
    };
providers.$compile.component(...)
loader.initModules([...], module)

data.js

formatters.js

f-log-view-menu.js

f-log-details.js

/ng/directives/menu/templates.js

Call loader.initModules() inside the register() function to declutter app.js

data.js

return function(providers, loader) {
    providers.$compile.component({
        fLogDetails: fLogDetails
    });

    return loader.initModules([
        'f-log-detail-section',
        'data',
        '/ng/directives/virtual_scroll'
    ], module);
};
providers.$compile.component(...)

f-log-details.js

loader.initModules([...], module)

f-log-detail-section.js

/ng/directives/virtual_scroll.js

return function(providers, loader) {
    providers.$compile.component({
        fLogDetailSection: fLogDetailSection
    });
    return loader.initModules([
        'f-log-detail-datum'
    ], module);
};
providers.$compile.component(...)

f-log-detail-datum.js

return function(providers) {
    providers.$compile.component({
        fLogDetailDatum: fLogDetailDatum
    });
};
loader.initModules([...], module)
providers.$compile.component(...)

Components

// new 'template directive' best practice:
.directive('f-log-menu', function fLogViewMenuFactory() {
  return {
    restrict: 'E',
    scope: {
        menu: '=',
        logView: '='
    },
    bindToController: true,
    controllerAs: 'logViewMenu',
    controller: LogViewMenu,
    templateUrl: loader.base_path('menu.html', module)
  };
});
// component uses best practices by default!
.component('counter', {
    bindings: {
        menu: '=',
        logView: '='
    },
    controllerAs: 'logViewMenu', // default '$ctrl'
    controller: LogViewMenu,
    templateUrl: function(loader) {
        return loader.base_path('menu.html', module);
    }
});

crafting an interface

Old way:

  • ng-controller="SomeController"
  • Spray variables into $scope
  • ng-template="some-template.html"
  • Scoop variables out of $scope

CONTROLLERAS

(And bindTocontroller)

The controller instance becomes a powerful interface, instead of a heap of variable assignments.

enter:

  • Properties related to each controller are isolated to the owning controller
  • $scope contains only controllers
  • Component inputs and outputs are declared
  • Developers know where to look to find the declaration (searchable)

Important notes

  • Components aren't replacements for directives. A component is a special type of directive that organizes a controller with a template.
     

  • Components do not have a link function and controllers still are not where you'd handle DOM manipulation.
     

  • If you need DOM manipulation, your component can use other directives that include that DOM manipulation in a link function.
     

  • Components are directives but not all directives  need to be components!

stateless components

not every component needs a controller.

What if I told you:

    var fLogDetailDatum{
            bindings: {
                /**
                 * @type {Datum[]}
                 */
                datum: '>',
                /**
                 * Array of language string prefixes to try in order when translating datum names.
                 * @type {String[]}
                 */
                namePrefixes: '>'
            },
            template: [
                '<td class="datum-name" data-name="{{ datum.name }}">',
                '    <f-icon class="{{:: $ctrl.datum.icon }}" ng-if="$ctrl.datum.icon"></f-icon>',
                '    <span ng-if="datum.name">{{:: $ctrl.datum.name | langPrefixed:$ctrl.namePrefixes }}</span>',
                '</td>',
                '<td class="datum-value">',
                '    <f-icon class="{{ $ctrl.datum.valueIcon }}" ng-if="$ctrl.datum.valueIcon"></f-icon>',
                '    <span ng-bind-html="$ctrl.datum.getValue ? $ctrl.datum.getValue() : datum.value"></span>',
                '</td>'
            ].join('\n');
        };
    }
    var fLogDetailDatum{
            bindings: {
                /**
                 * @type {Datum[]}
                 */
                datum: '>',
                /**
                 * Array of language string prefixes to try in order when translating datum names.
                 * @type {String[]}
                 */
                namePrefixes: '>'
            },
            template: [
                '<td class="datum-name" data-name="{{ datum.name }}">',
                '    <f-icon class="{{:: $ctrl.datum.icon }}" ng-if="$ctrl.datum.icon"></f-icon>',
                '    <span ng-if="datum.name">{{:: $ctrl.datum.name | langPrefixed:$ctrl.namePrefixes }}</span>',
                '</td>',
                '<td class="datum-value">',
                '    <f-icon class="{{ $ctrl.datum.valueIcon }}" ng-if="$ctrl.datum.valueIcon"></f-icon>',
                '    <span ng-bind-html="$ctrl.datum.getValue ? $ctrl.datum.getValue() : datum.value"></span>',
                '</td>'
            ].join('\n');
        };
    }

 

clu

 

Transclusion allows us to create very customizable components.

Trans

 

sion

 Provides a mechanism to wrap a template fragment inside a component

 

clu

 

The new <f-dialog> directive uses transclusion to wrap it's content with dialog html

Trans

 

sion

 

clA
CLB
clC

<a-slot>Angular JS 1.5</a-slot>

Trans

 

 

 

sion

<b-slot>Allows multiple transclusion slots</b-slot>

<c-slot>For more reuseable components</c-slot>

<ng-transclude ng-transclude-slot="a-slot">

Angular JS 1.5

</ng-transclude>

<ng-transclude ng-transclude-slot="b-slot">

Allows multiple transclusion slots

</ng-transclude>

<ng-transclude ng-transclude-slot="c-slot">

For more reuseable components

</ng-transclude>

DI Binding

AngularJS leverages Dependency Injection heavily. But it can result in massive constructor functions

function MyController($scope, greeter) {
  this.sayHello = function() {
    greeter.greet('Hello World');
  };
  // add 20 other methods
}
function MyController($scope, greeter) {
    this._greeter = greeter;
}

MyController.prototype = {
    sayHello: function() {
        this._greeter.greet('Hello World');
    }
    // 12 other methods that need greeter, $scope, etc
}

It's better for efficiency and readability to place instance methods on the prototype.

function MyController($scope, injector) {
    injector.injectMarked(this, {$scope: $scope});
}

MyController.prototype = {
    sayHello: inject.mark(function sayHello(greeter) {
        greeter.greet('Hello World');
    }),
    // 12 other methods that manage
    // their own dependency injection!
}

Maintenence Nightmare

Doesn't Scale

UNREADABLE
INEFFICIENT

Solution: Custom 'decorator'.

Solution: Custom 'decorator'.

  • ng/services/injector provides inject rjs module with the mark decorator.
  • Wrap methods with inject.mark()
  • Call injector.injectMarked(this, locals) in the constructor
  • Be sure to supply a named function expression to inject.mark() so that code editors can find it.

models

Model View Controller

AngularJS has excellent support for:

 

  • View (templates)
  • Controller
  • Model?

?

$resource and $http provide:

POJO

 

  • Plain
  • Old
  • Javascript
  • Object

{}

Getting the OO back

CMDB service provides:

  • $resource-like interface
  • CMDB.Model to add prototype based on CMDB path + name
  • Reset after $routeChange event

CMDB data:

  • Raw values
  • Odd layout
  • Often doesn't match GUI requirements

Adding behavior to models promotes:

  • Loose coupling
  • Lean controllers
  • Encapsulation
  • Separation of concerns

Enhancing A Model

define(['./models', 'ngModule'],
function(models, ngModule) {
    function VPNEdit(CMDB) {
        CMDB.model(models);
        this.phase1 = new CMDB('vpn.ipsec', 'phase')
                        .get(mkey);
    }
    
    ngModule.component('f-vpn-edit', {
        controllerAs: 'vpnEdit',
        controller: VPNEdit
    });
});
define(function() {
    var p1Prototype = {
        valueGetterSetter: function(value) {
            if (arguments.length > 0) {
                this._value = value;
                this.updateState();
            }
            return this.value;
        },
        _updateState: function(value) {
            this._state.value = value;
        }
    };

    // note that promise will be resolved after all CMDB rows have been fetched.
    function enhance(value, promise) {
        // it is not necessary to wait for promise in most situations.
        value._state = {};
        promise.then(function() {
            value._updateState(Math.random());
        });
    }

    // add additional 'static' properties to the schema.
    function enhanceSchema(schema) {
        schema.children.value.$options = schema.children.value.options.filter(notBad);

        function notBad(option) { return option.name === 'bad' }
    }

    // return an array of CMDB.Model objects for specific path/name combinations
    return function(CMDB) {
        return [new CMDB.Model('vpn.ipsec', 'phase1', p1Prototype, enhance, enhanceSchema)];
    };
});
<input type="text"
    ng-model="vpnEdit.phase1.valueGetterSetter"
    ng-model-options="{getterSetter: true }">

AngularJS 2

  • Still has directives
  • @Component() decorator
    • styles bound to component!
  • @Directive() decorator
  • TypeScript! (optional)
$routeConfig: [
  { path: '/heroes/...', component: 'someComponent' },
  ...
]
...
.component('heroes', {
    $routeConfig: [
      {path: '/', name: 'HeroList',
        component: 'heroList', useAsDefault: true},
      {path: '/:id', name: 'HeroDetail',
        component: 'heroDetail'}
    ]
  })

Gradual transition roadmap

  • Components
    • Similar to angular 2 components
  • Component Router
    • Available in angular 1.5!
    • provides routing on a component level for nested views

Angular 1.5

By Jamie Pate

Angular 1.5

  • 1,560