Angular Style Guide for enterprise and large-scale projects

Standard

GitHub Repository:  GitHub Link

Angular Style Guide for enterprise and large-scale projects

Single Responsibility

  • Define 1 component per file.

pastedimage

IIFE

  • Wrap Angular components in an Immediately Invoked Function Expression (IIFE).

pastedimage 1

Definitions (aka Setters)

  • Declare modules without a variable using the setter syntax.
  • When using a module, avoid using a variable and instead use chaining with the getter syntax.
  • Only set once and get for all other instances.

pastedimage 2

Named vs Anonymous Functions

  • Use named functions instead of passing an anonymous function in as a callback.

pastedimage 3

controllerAs View Syntax

  • Use the controllerAs syntax over the classic controller with $scope syntax.
  • Use a capture variable for this when using the controllerAs syntax. Choose a consistent variable name such as vm, which stands for ViewModel.
  • When a controller must be paired with a view and either component may be re-used by other controllers or views, define controllers along with their routes.

pastedimage 4 pastedimage 5

Route Resolve Promises

  • When a controller depends on a promise to be resolved before the controller is activated, resolve those dependencies in the $stateProvider before the controller logic is executed. If you need to conditionally cancel a route before the controller is activated, use a route resolver.

pastedimage 6

Bindable Members Up Top

  • Place bindable members at the top of the controller, alphabetized, and not spread through the controller code.
  • Resolve start-up logic for a controller in an activate function.

pastedimage 7

Function Declarations to Hide Implementation Details

  • Use function declarations to hide implementation details. Keep your bindable members up top. When you need to bind a function in a controller, point it to a function declaration that appears later in the file. This is tied directly to the section Bindable Members Up Top
  • Resolve start-up logic for a controller in an activate function.

pastedimage 8

Separate Data Calls

  • Refactor logic for making data operations and interacting with data to a factory. Make data services responsible for XHR calls, local storage, stashing in memory, or any other data operations.

pastedimage 9

Directives

  • Create one directive per file. Name the file for the directive.
  • When manipulating the DOM directly, use a directive.
  • When creating a directive that makes sense as a stand-alone element, allow restrict E (custom element) and optionally restrict A (custom attribute).
  • Use controller as syntax with a directive to be consistent with using controller as with view and controller pairings.
  • Use bindToController = true when using controller as syntax with a directive when you want to bind the outer scope to the directive’s controller’s scope.

pastedimage 10

Manual Annotating for Dependency Injection

  • Avoid using the shortcut syntax of declaring dependencies without using a minification-safe approach. Use $inject to manually identify your dependencies for Angular components.

pastedimage 11

Startup Logic

  • Inject code into module configuration that must be configured before running the angular app. Ideal candidates include providers and constants.
  • Any code that needs to run when an application starts should be declared in a factory, exposed via a function, and injected into the run block.

pastedimage 12

Module Dependencies

  • The application root module depends on the app specific feature modules and any shared or reusable modules.

pastedimage 13

Application Structure LIFT Principle

  • LIFT is:
  • Locating our code is easy
  • Identify code at a glance
  • Flat structure as long as we can
  • Try to stay DRY (Don’t Repeat Yourself) or T-DRY

Folders-by-Feature Structure:

  • Create folders named for the feature they represent. When a folder grows to contain more than 7 files, start to consider creating a folder for them. Your threshold may be different, so adjust as needed.

pastedimage 14

Integration of Typescript in AngularJS

Standard

GitHub Repository:  GitHub Link

AngularJS is a JavaScript Model-View-Whatever (MVW) framework that allows the writing of dynamic Single Page web Applications, and is becoming widely embraced because of its simplicity and completeness. Amongst its exhaustive list of features, the framework includes Dynamic Data Binding, Client-side Routing, Unit and End2End Testing or even HTML language extension.

Typescript, on the other hand, is a typed superset of the JavaScript language that is compiled to produce clean, simple cross-browser compatible JavaScript code, which can help when building and maintaining large-scale applications. Typescript follows ECMAScript 5 (ES5) syntax but also includes several proposed features of ES6 such as interfaces, classes and modules.

Please install Web Essentials for VS 2013 for rending JS file for TS on side by side while typing

https://visualstudiogallery.msdn.microsoft.com/56633663-6799-41d7-9df7-0f2a504ca361

Migrating AngularJS to Typescript

Entity class

2015-10-29_10-43-25

module App.Entity {
    "use strict";

    export interface ICandidate {
        candidate_id: number;
        first_name: string;
        middle_initial: string;
        last_name: string;
        email: string;
        expected_salary: number;

    };
}

Defining Application Modules

2015-10-29_10-49-17

((): void => {
    "use strict";

    angular
        .module("onBoardingApp", [
            "onBoardingApp.Core",
            "onBoardingApp.Module"
        ]);

    angular
        .module("onBoardingApp.Core", [
            "ui.router",
            "oc.lazyLoad",
            "breeze.angular"
        ]);

    angular
        .module("onBoardingApp.Module", [
            "onBoardingApp.home",
        // "onBoardingApp.candidate",
            "onBoardingApp.layout"
        ]);
})();

Defining Application Configuration

2015-10-29_10-57-49

((): void => {
    "use strict";

    angular
        .module("onBoardingApp")
        .config(config);

    config.$inject = ["$stateProvider"];

    function config($stateProvider: ui.IStateProvider): void {
        $stateProvider
            .state("adminHome", {
                url: "/",
                controller: "HomeController",
                controllerAs: "vm",
                templateUrl: "/Scripts/appTS/modules/home/views/onBoardingApp.home.html",
                resolve: {
                    getSecondaryCandidateService: getSecondaryCandidateService
                }
            })
            .state("allCandidates", {
                url: "/candidates",
                controller: "CandidateController",
                controllerAs: "vm",
                templateUrl: "/Scripts/appTS/modules/candidate/views/onBoardingApp.candidate.html",
                resolve: {
                    allCandidates: function ($ocLazyLoad: oc.ILazyLoad): any {
                        return $ocLazyLoad.load([
                            {
                                name: "onBoardingApp.candidate",
                                files: ["/Scripts/appTS/modules/candidate/onBoardingApp.candidate.js"],
                                cache: false
                            },
                            {
                                name: "onBoardingApp.candidate.controllers",
                                files: ["/Scripts/appTS/modules/candidate/js/onBoardingApp.candidate.controller.js"],
                                cache: false
                            },
                            {
                                name: "onBoardingApp.candidate.services",
                                files: ["/Scripts/appTS/modules/candidate/js/onBoardingApp.candidate.services.js"],
                                cache: false
                            }
                        ]);
                    }
                }
            });

        function getSecondaryCandidateService(homeService: App.Home.IHomeService): ng.IPromise<App.Entity.ICandidate[]> {
            return homeService.getAllSecondaryCandidates();
        }
    }

})();

Defining Shared Services

2015-10-29_11-47-00

module App.Service {
    "use strict";

    export interface IWebApi {
        withParameter: (methodType: string, webApiUrl: string, parameterObject: any) => any;
        nonParameter: (methodType: string, webApiUrl: string) => any;
    }

    webApi.$inject = ["$http", "$templateCache", "$q"];

    function webApi($http: ng.IHttpService, $templateCache: ng.ITemplateCacheService, $q: ng.IQService): IWebApi {

        var factory: IWebApi = {
            withParameter: withParameter,
            nonParameter: nonParameter
        };

        return factory;

        function withParameter(methodType, webApiUrl, parameterObject) {
            var deferred = $q.defer();
            $http({
                method: methodType,
                url: webApiUrl, 
                data: parameterObject,
                cache: $templateCache
            })
                .success(deferred.resolve)
                .error(deferred.resolve);
            return deferred.promise;
        }

        function nonParameter(methodType, webApiUrl) {
            var deferred = $q.defer();
            $http({
                method: methodType,
                url: webApiUrl,
                cache: $templateCache
            })
                .success(deferred.resolve)
                .error(deferred.resolve);
            return deferred.promise;
        }
    }

    angular
        .module("onBoardingApp")
        .factory("webApi", webApi);
}

Defining Module specific services

2015-10-29_12-05-16

module App.Home {
    "use strict";

    export interface IHomeService {
        getAllSecondaryCandidates: () => ng.IPromise<App.Entity.ICandidate[]>;
    }

    HomeService.$inject = ["webApi"];

    function HomeService(webApi: App.Service.IWebApi): IHomeService {

        var factory: IHomeService = {
            getAllSecondaryCandidates: getAllSecondaryCandidates
        };

        return factory;

        function getAllSecondaryCandidates() {
            return webApi.nonParameter('GET', '/Home/GetSecondaryCandidateData');
        }
    }

    angular
        .module("onBoardingApp.home")
        .factory("homeService", HomeService);
}

Defining Module Controller

2015-10-29_12-32-20

module App.Candidate {
    "use strict";

    interface ICandidateController {
        candidates: App.Entity.ICandidate[];
        lazyLoadParams: string[];
        tempValueForUnitTest: string;
        showAction: boolean;
        currentEdit: App.Entity.ICandidate;
        itemToEdit: App.Entity.ICandidate;
        newCandidate: App.Entity.ICandidate;
        activate: () => void;
        getCandidates: () => ng.IPromise<App.Entity.ICandidate[]>;
        findCandidate: (id: number) => number;
        add: () => void;
        edit: (candidate: App.Entity.ICandidate) => App.Entity.ICandidate;
        cancelEdit: (candidateId: number) => void;
        save: (candidate: App.Entity.ICandidate) => App.Entity.ICandidate[];
        testFunctionForUnitTesting: () => string;

    }

    class CandidateController implements ICandidateController {

        candidates: App.Entity.ICandidate[];
        lazyLoadParams: string[];
        tempValueForUnitTest: string;
        showAction: boolean;
        currentEdit: App.Entity.ICandidate;
        itemToEdit: App.Entity.ICandidate;
        newCandidate: App.Entity.ICandidate;

        static $inject: string[] = ["candidateService"];

        constructor(private candidateService: ICandidateService) {
            this.activate();
        }

        activate() : void {
            this.lazyLoadParams = [
                '/Scripts/appTS/shared/directive/candidateGrid.js',
                '/Scripts/appTS/shared/directive/maxLengthForNumbers.js',
                '/Scripts/appTS/shared/directive/validemail.js'
            ];
            this.tempValueForUnitTest = "testing123";
            this.showAction = true;
            this.itemToEdit = <App.Entity.ICandidate>{};
            this.currentEdit = <App.Entity.ICandidate> {};
            this.getCandidates();
            
        }

        getCandidates(): ng.IPromise<App.Entity.ICandidate[]> {
            return this.candidateService.getAllCandidates()
                .then((response: ng.IHttpPromiseCallbackArg<App.Entity.ICandidate[]>): App.Entity.ICandidate[] => {
                    this.candidates = <App.Entity.ICandidate[]>response
                    return this.candidates;
                });
        }

        findCandidate = (id: number): number => {
            for (var i in this.candidates) {
                if (this.candidates[i].candidate_id == id)
                    return i;
            }
        };

        add(): void {
            this.candidates.push(this.newCandidate);
            this.newCandidate = null;
        };

        edit = (candidate: App.Entity.ICandidate): App.Entity.ICandidate => {
            this.currentEdit[candidate.candidate_id] = true;
            this.itemToEdit = angular.copy(candidate);
            return this.itemToEdit;
        };

        cancelEdit = (candidateId: number): void => {
            console.log(this)
            this.currentEdit[candidateId] = false;
        };

        save = (candidate: App.Entity.ICandidate): App.Entity.ICandidate[] => {
            var c = this.findCandidate(candidate.candidate_id);
            this.candidates[c] = this.itemToEdit;
            this.currentEdit[candidate.candidate_id] = false;
            return this.candidates;
        };

        testFunctionForUnitTesting(): string {
            return "test";
        }
    }

    angular
        .module("onBoardingApp.candidate")
        .controller("CandidateController", CandidateController);
}

Defining Directive consists of DOM manipulation logic

2015-10-29_12-57-18

module App.Directive {
    "use strict";

    interface Ivalidmail extends ng.IDirective {
    }

    interface IvalidmailScope extends ng.IScope {
    }

    interface IvalidmailAttributes extends ng.IAttributes {
    }

    validmail.$inject = ["$window"];

    function validmail($window: ng.IWindowService): Ivalidmail {
        return {
            restrict: 'A',
            scope: {},
            controller: validEmailController,
            controllerAs: 'vm',
            link: link,
            bindToController: true,
        }

        function link(scope: IvalidmailScope, element: ng.IAugmentedJQuery, attrs: IvalidmailAttributes) {
            element.bind('blur', (): void => {
                var regex = /\S+@\S+\.\S+/;
                element.css('background-color', !regex.test(element.val()) ? 'yellow' : '');
            });
        }

        function validEmailController() {
            var vm = this;

        }
    }

    angular
        .module("onBoardingApp")
        .directive("validEmail", validmail);
}

Defining Directive consists of business logic

2015-10-29_11-29-43

module App.Directive {
    "use strict";

    interface IcandidateGridScope extends ng.IScope {
        vm: any;
        datasource: App.Entity.ICandidate[];
        showaction: boolean;
        itemtoedit: any;
        currentedit: any;

    }

    interface IcandidateGridAttributes extends ng.IAttributes {
    }

    interface ICandidateGridController {
        candidates: App.Entity.ICandidate[];
        itemToEdit: App.Entity.ICandidate;
        currentEdit: App.Entity.ICandidate;
        activate(): void;
        cancelEdit(candidateId: number): void;
        edit(candidate: App.Entity.ICandidate): void;
        save(candidate: any): void;
    }

    class CandidateGridController implements ICandidateGridController {

        candidates = [];
        itemToEdit = <App.Entity.ICandidate>{};
        currentEdit = <App.Entity.ICandidate>{};

        static $inject = ['$scope'];

        constructor(public $scope: IcandidateGridScope) {
            this.activate();
        }

        activate(): void {
            this.candidates = this.$scope.vm.datasource;
            this.currentEdit = this.$scope.vm.currentedit;
        }

        cancelEdit(candidateId: number): void {
            this.$scope.vm.canceledit()(candidateId);
        };

        edit(candidate: App.Entity.ICandidate): void {
            this.itemToEdit = this.$scope.vm.editcandidate()(candidate);
        };

        save(candidate: App.Entity.ICandidate): void {
            this.candidates = this.$scope.vm.savecandidate()(candidate);
        };
    }

    export class candidateGrid implements ng.IDirective {
        static instance(): ng.IDirective {
            return new candidateGrid;
        }

        restrict = 'EA';
        templateUrl = 'scripts/app/shared/directive/template/candidateGrid.html';
        scope = {
            datasource: '=',
            currentedit: '=',
            itemtoedit: '=',
            showaction: '@',
            canceledit: '&',
            editcandidate: '&',
            savecandidate: '&',
            addcandidate: '&'
        };
        controller = CandidateGridController;
        controllerAs = 'vm';
        bindToController = true;
        transclude = true;

        link(scope: IcandidateGridScope, element: ng.IAugmentedJQuery, attrs: IcandidateGridAttributes) {

        };

    }

    angular
        .module("onBoardingApp")
        .directive("candidateDataGrid", candidateGrid.instance);
}

 

Jasmine Unit Test For AngularJS sample using Reshaprer and Chutzpa as test runner

Standard

GitHub Repository:  GitHub Link

Jasmine Unit Test For AngularJS sample using Reshaprer and Chutzpa as test runner

For installing Chutzpaa:

For installing Resharper:

pastedimage 2

Jasmine Cheatsheet

describe("A suite", function() {
  it("contains spec with an expectation", function() {
    expect(true).toBe(true);
  });
});

Expectations

expect(true).toBe(true)
expect(true).not.toBe(true)

expect(a).toEqual(bar)

expect(message).toMatch(/bar/)
expect(message).toMatch('bar')

expect(a.foo).toBeDefined()
expect(a.foo).toBeUndefined()
expect(a.foo).toBeNull()

expect(a.foo).toBeTruthy()
expect(a.foo).toBeFalsy()

expect(message).toContain('hello')

expect(pi).toBeGreaterThan(3)
expect(pi).toBeLessThan(4)
expect(pi).toBeCloseTo(3.1415, 0.1)

expect(func).toThrow()

Blocks

beforeEach(function() { ... });
afterEach(function() { ... });

Pending

xit("this is a pending test", function() { ... })
xdescribe("this is a pending block", function() { ... })

Spies

spyOn(foo, 'setBar')
spyOn(foo, 'setBar').andReturn(123)
spyOn(foo, 'getBar').andCallFake(function() { return 1001; })
foo.setBar(123)

expect(foo.setBar).toHaveBeenCalled()
expect(foo.setBar).toHaveBeenCalledWith(123)
expect(foo.setBar.calls.length).toEqual(2)
expect(foo.setBar.calls[0].args[0]).toEqual(123)

Creating spies

stub = jasmine.createSpy('stub')
stub("hello")

expect(stub.identity).toEqual("stub")
expect(stub).toHaveBeenCalled()

Async

it("should run async", function() {
  var flag = false, value = 0;

  runs(function() {
    setTimeout(function() { flag = true; }, 500);
  });

  waitsFor(function() {
    value++;
    return flag;
  }, "increment", 750);

  runs(function() {
    expect(value).toBeGreaterThan(0);
  });
});

HTML runner

var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 250;

var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);

$(function() { jasmineEnv.execute(); });

Jasmine jQuery

Jasmin jQuery.

expect($('#id')).toBe('div')
expect($('input[type=checkbox]')).toBeChecked()
expect($('input[type=checkbox]')).toBeDisabled()
expect($('input[type=checkbox]')).toBeFocused()
expect($('#menu ul')).toBeEmpty()

expect($('#toolbar')).toBeHidden()
expect($('#toolbar')).toBeVisible()

expect($('#popup')).toHaveCss({ margin: "10px" })
expect($('option')).toBeSelected()

expect($('.foo')).toExist()

expect($('a')).toHaveAttr('rel')
expect($('a')).toHaveAttr('rel', 'nofollow')

expect($('a')).toHaveClass('rel')
expect($('a')).toHaveId('home')

expect($('a')).toHaveHtml('<span></span>')
expect($('a')).toContainHtml('<span></span>')
expect($('a')).toHaveText('hi')

expect($form).toHandle('submit') // event
expect($form).toHandleWith('submit', onSumbit)

Event spies

spyOnEvent($('#some_element'), 'click');
$('#some_element').click();
expect('click').toHaveBeenPreventedOn($('#some_element'));
expect('click').toHaveBeenTriggeredOn($('#some_element'));

Below images will highlight new changes in ngninja project

  • Directory Structure
  • Resharper test Runner
  • Chutzpa test runner

pastedimage

How to code Jasmine Test Case

  • Mention test file name
  • Mock all dependencies for module which you are testing
  • mention angular module name for which you are writing test cases
  • Inject dependencies and initialize all variables
  • Write unit test for each functionality

P.S. I have written unit test cases for Candidate Module(Controller, Service, main Candidate file and Shared http service, total 18 Unit test cases)

pastedimage 1

/// <reference path="../../../../lib/angular/angular.js" />
/// <reference path="../../../../lib/angular/angular-mocks.js" />
/// <reference path="../js/onBoardingApp.candidate.controller.js" />

describe('onBoardingApp-candidate-controllers', function () {
    var candidateService, controller;

    beforeEach(function () {

        module('onBoardingApp.candidate.controllers');

        module(function ($provide) {
            $provide.value('candidateService', {
                getAllCandidates: function () {
                    return {
                        then: function () { return {}; },

                    }
                }
            });
        });

        inject(function ($controller, _candidateService_) {
            candidateService = _candidateService_;
            controller = $controller("CandidateController", {});
        });
    });

    it('should contain factory', function () {
        expect(candidateService).not.toEqual(null);
    });

    it('should return tempValueForUnitTest', function () {
        expect(controller.tempValueForUnitTest).toEqual("testing123");
    });

    it('should return testFunctionForUnitTesting function', function () {
        expect(controller.testFunctionForUnitTesting()).toEqual("test");
    });

    it('should add candidate record', function () {
        controller.candidates = [];
        controller.newCandidate =
        {
            'candidate_id': '1',
            'first_name': 'Scott',
            'middle_initial': 'AA',
            'last_name': 'Gurthie',
            'email': 'h@aa.com',
            'expected_salary': '15000'
        };        
       controller.add();
       expect(controller.candidates[0].email).toEqual('h@aa.com');
    });

    it("should cancel candidate's record to be updated", function () {
        controller.cancelEdit(2);
        expect(controller.currentEdit[2]).toBeFalsy();
    });

    it('should edit candidate record', function () {
        var candidate =
        {
            'candidate_id': '1',
            'first_name': 'Scott',
            'middle_initial': 'AA',
            'last_name': 'Gurthie',
            'email': 'h@aa.com',
            'expected_salary': '15000'
        };
        controller.edit(candidate);
        expect(controller.itemToEdit.first_name).toBe('Scott');
    });

    it("should find candidate by id", function () {

        controller.candidates = [
            {
                'candidate_id': '1',
                'first_name': 'Scott',
                'middle_initial': 'AA',
                'last_name': 'Gurthie',
                'email': 'h@aa.com',
                'expected_salary': '15000'
            }, {
                'candidate_id': '2',
                'first_name': 'Brett',
                'middle_initial': 'BB',
                'last_name': 'John',
                'email': 'h@bb.com',
                'expected_salary': '55000'
            }
        ];

        expect(controller.findCandidate(2)).toEqual('1');
    });

    it('should save candidate record after update', function () {
        var candidate =
        {
            'candidate_id': '1',
            'first_name': 'Scott111',
            'middle_initial': 'AA111',
            'last_name': 'Gurthie111',
            'email': 'h@aa111.com',
            'expected_salary': '15000111'
        };
        controller.edit(candidate);
        controller.save(candidate);
        expect(controller.itemToEdit.first_name).toBe('Scott111');
    });

});

Lazy Registration of modules of AngularJS for large scale application

Standard

GitHub Repository:  GitHub Link

Lazy Registration of modules of AngularJS for large scale application

When building large sites or apps with many routes/views in AngularJS, it would be good to not have to load all artefacts such as controllers, directives etc., on the first load. Ideally, on first load, only the artefacts that are needed for the route in question, will be loaded. This may be either in one download or multiple depending on the app, however, it will only be what is needed to render the particular route. Then as the user navigates the app by changing the route, other artefacts that have not already been loaded, will then be loaded as and when there are needed. Not only should this potential speed up the initial page load, but it should also result in bandwidth not being wasted.

pastedimage 12

After clicking on Users menu item at top of webpage

pastedimage 22

Code Demonstration (AppConfig.js) :

 

pastedimage 2

 

angular
    .module('onBoardingApp')
    .config(configure)
    .run(runBlock);

runBlock.$inject = ['$state'];
configure.$inject = ['$stateProvider'];

function runBlock($state) {
    $state.go('adminHome');
};

function configure($stateProvider) {
    $stateProvider
        .state('adminHome', {
            url: '/',
            controller: 'HomeController',
            controllerAs: 'vm',
            templateUrl: '/Scripts/app/modules/home/views/onBoardingApp.home.html',
            resolve: {
                getSecondaryCandidateService: getSecondaryCandidateService
            }
        })
        .state('allCandidates', {
            url: '/candidates',
            controller: 'CandidateController',
            controllerAs: 'vm',
            templateUrl: '/Scripts/app/modules/candidate/views/onBoardingApp.candidate.html',
            resolve: {
                allCandidates: function ($ocLazyLoad) {
                    return $ocLazyLoad.load([{
                        name: 'onBoardingApp.candidate',
                        files: ['/Scripts/app/modules/candidate/onBoardingApp.candidate.js'],
                        // {cache: false, timeout: 5000}
                        cache: false
                    }, {
                        name: 'onBoardingApp.candidate.controllers',
                        files: ['/Scripts/app/modules/candidate/js/onBoardingApp.candidate.controller.js'],
                        cache: false
                    },
                    {
                        name: 'onBoardingApp.candidate.services',
                        files: ['/Scripts/app/modules/candidate/js/onBoardingApp.candidate.services.js'],
                        cache: false
                    }]);
                }
            }
        });

    function getSecondaryCandidateService(homeService) {
        return homeService.getAllSecondaryCandidates();
    }
};