REAL WORLD AngularJS

Yaniv Efraim

About Myself

I am a senior Front End developer @Supersonic

Developing for the web for more than 10 years

Love Javascript

 

 

Agenda

  1. AngularJS pro tips

  2. Live Code & Demo

  3. Q&A

Pro Tips

Part 1

Tip 1: Application structure best practices


app/
    app.module.js
    app.config.js
    controllers/
        attendees.js            
        session-detail.js       
        ...          
    directives/       
        calendar.directive.js  
        calendar.directive.html  
        ...
    services/       
        dataservice.js  
        localstorage.js
        ...
    views/
        attendees.html     
        session-detail.html
        sessions.html      
        ...

Folder-by-type (Avoid)

Folder-by-Feature

app/
----- components/   //reusable components 
---------- sidebar/
--------------- sidebar.module.js
--------------- sidebarDirective.js
--------------- sidebarView.html
--------------- _sidebar.scss
--------------- sidebar_test.js
---------- article/
--------------- article.module.js
--------------- articleDirective.js
--------------- articleView.html
--------------- ...
----- pages/   // acts as pages or partials of our site
---------- home/
--------------- home.module.js    
--------------- homeController.js
--------------- homeService.js
--------------- homeView.html
--------------- _home.scss
--------------- home_test.js
----- app.module.js
----- app.routes.js
index.html

Use many small, self contained modules

Each component should have its own module:

---------- home/
--------------- home.module.js
--------------- home.controller.js
--------------- home.service.js
--------------- home.view.html
--------------- home.css
--------------- home_test.js


angular.module('home',[]);//home.module.js

angular.module('home').controller('HomeCtrl', HomeCtrl);//home.controller.js

angular.module('home').factory('homeService', homeService);//home.service.js

Use many small, self contained modules

Root module should be a container referencing all depending modules:

angular.module('app',[
        //Angular Modules
        'ngRoute',
        //Custom modules
        'sidebar',
        'article',
        'home',
        'blog',
        //3rd Party Modules
        'ui.bootstrap'
]);

Advantages

  • Dramatically easier to find what you are looking for
     
  • Modules are reusable
     
  • Easier for several developers to work on same project
     
  • Easier to remove components
     
  • Application can grow easily

Further reading: 

Tip 2: Each controller should mind his own business!

$scope inheritance == Semi-Global Data

  • Hard to track where data is coming from (data is shared between controllers)
     
  • Scope's value changes can affect places you did not intend to affect. 
     
  • Harder to refactor
     
  • The "dot rule" (example in few slides)
     
  • Controllers are coupled

Solutions

  1. Prefer using ControllerAs syntax (Angular 1.2+).
    This will create new "isolated" context for your
    controller (which is not affected by parent controllers)

     
  2. Use "Components Pattern"

Using ControllerAs syntax, we are creating an instance:

<div ng-controller="MainCtrl as main">
   {{ main.title }}
</div>

angular.module('myApp', [])
        .controller("MainCtrl", MainCtrl);

function MainCtrl() {
    this.title = "My Title";
    this.sendMessage = function() { };
}

Solution 1: ControllerAs

ControllerAs

Or even better:

<div ng-controller="MainCtrl as main">
   {{ main.title }}
</div>

angular.module('myApp', [])
        .controller("MainCtrl", MainCtrl);

function MainCtrl() {
    var vm = this;// vm stands for 'viewmodel' 
    vm.title = "My Title";
    vm.sendMessage = function() { };
}

How ControllerAs is implemented behind the scenes

ControllerAs - Nested controllers (the "dot rule")

(Problem)

(Solution)

Demo

Other benefits to this approach

  • Removes the use of $scope when no need for special operations (like $scope.$broadcast). This is a good preparation for AngularJS V2.
     
  • Helps avoid the temptation of using $scope methods inside a controller when it may otherwise be better to avoid them or move them to a factory 
     
  • Syntax is closer to that of a "vanilla" JavaScript constructor

Solution 2: Components Pattern

Avoid using controllers. Use directives instead:

Further reading: 

Tip 3: Use Angular Services as your "Model"

Why business logic / app state inside controllers is bad?

  • Controllers instantiated for each view and dies when the view unloads
     
  • Controllers are not reusable - they are coupled with the view
     
  • Controllers are not meant to be injected

Controller's job is to talk to the "Model" 

(don't use it as a model)

angular.module('Store', [])  
.controller('OrderCtrl', function(Products) {
    this.products = Products.query();

    this.items = [];

    this.addToOrder = function(item) {
        this.items.push(item);
    };

    this.removeFromOrder = function(item) {
        this.items.splice(this.items.indexOf(item), 1);//-->Business logic inside controller
    };

    this.totalPrice = function() {
        return this.items.reduce(function(memo, item) {
            return memo + (item.qty * item.price);//-->Business logic inside controller
        }, 0);
    };
});

Delegate business logic to a Service

angular.module('Store')  
.factory('Order', function() {    
    var add = function(item) {
        this.items.push(item);
    };

    var remove = function(item) {
        if (this.items.indexOf(item) > -1) {
          this.items.splice(this.items.indexOf(item), 1);  
        }
    };

    var total = function() {
        return this.items.reduce(function(memo, item) {
            return memo + (item.qty * item.price);
        }, 0);
    };

    return {
        items: [],
        addToOrder: add,
        removeFromOrder: remove,
        totalPrice: total
    };
});

Now the controller is doing its job: talking to the Model

angular.module('Store', [])  
.controller('OrderCtrl', function(Products, Order) {
    this.products = Products.query();
    this.items = Order.items;

    this.addToOrder = function(item) {
        Order.addToOrder(item);
    };

    this.removeFromOrder = function(item) {
        Order.removeFromOrder(item);
    };

    this.totalPrice = function() {
        return Order.total();
    };
});

Further reading: 

Tip 4: Avoid Globals

var app = angular
            .module('app')
            .controller('HomeController', HomeController);

function HomeController(){
    var vm = this;
    vm.name = 'my name';
};

(How many globals do you see here?)

Use Immediately Invoked Function Expression (IIFE) 

(function(){
    var app = angular
                .module('app')
                .controller('HomeController', HomeController);
    
    function HomeController(){
        var vm = this;
        vm.name = 'my name';
    };
})();

TIP 5: Use one time databinding when possible

AngularJS 1.3+ "one time binding"

One-time expressions will stop recalculating once
they are stable, which happens after the first digest…

<p>Hello {{name}}!</p>

Becomes:

<p>Hello {{::name}}!</p>

Demo

Anguler 1.3+ :

Anguler <= 1.2 :

Tip 6: Automate minification annotation using ng-Annotate (Grunt/Gulp)

ng-annotate

app.controller('MyController', 
                ['serviceA', 'serviceB', function(serviceA, serviceB){
...
}])
    

Will become:

app.controller('MyController', myController);

function myController(serviceA, serviceB){
    ...
}
    

ng-annotate will automatically add:

myController.$inject = ["serviceA", "serviceB"];

The usual controller decleration:

ng-annotate

For complex scenarios explicitly use the @ngInject

app.controller('MyController', myController);

function myController(){
    ...
}
    
/* @ngInject */
myController.prototype.methodA = function(serviceA, serviceB){
...
}


ng-annotate will recognise the need for injection and add:

myController.prototype.methodA.$inject = ["serviceA", "serviceB"];

Tip 7: Precache html templates using html2js (Grunt/Gulp)

html2js

  • html2js parses all template files in your application and creates a javascript file that populates the $templateCache
     
  • The output is an Angular module that can be injected as a dependency into the main app module
     
  • ​The output javascript file can be concatenated into the main js file + minified + GZiped 

html2js usage  (example)

html2js output file example (auto-generated by html2js task):

angular.module("compiled-templates", []).run(["$templateCache", function($templateCache) {
  $templateCache.put("all-templates",
    "<script type=text/ng-template id=my-directive-template.html><div ng-controller=\"SomeCtrl\" class=\"some-class\" ng-click=\"performeClick()\">\n" +
    "        <div class=\"div-class\">\n" +
    "        ...
    "        </div>\n" +
    "    </div></script>);
});

Templates module should be added to main module:

angular.module('myApp', [ 'compiled-templates', /* other dependencies */ ]);

html2js usage  (example)

Compile generated templates in your main controller: 

var templatesHTML = $templateCache.get('all-templates');

$compile(templatesHTML)($rootScope);

Reference your templates as usual:

myApp.directive("myDirective", [function () {
      return{
        scope:{},
        restrict: 'E',
        templateUrl: 'my-directive-template.html',
        link: function(scope, element, attrs){
        },
        controller: ['$scope', function($scope){
        }]
    };
}]);

Further reading: 

Tip 8: Use Route's Resolve

app.config(function($routeProvider) {

    var getName = function(NameService) {
        return NameService.getName();
    };

    $routeProvider
       .when('/profile', {
           templateUrl: '/profile.html',
           controller: 'ProfileController',
           /* only navigate when we've resolved these promises */
           resolve: {
               name: getName,
               profile: getProfile,
               anythingElse: getAnythingElse
           }
   })
});

Tip 9: Cache service calls

var dataPromise = null;

function getData() {
    if (dataPromise == null)
        dataPromise = $http.get("data.json").then(function (res) {
           return res.data;
        });
    return dataPromise;
}

Tip 10: Decouple  your Controllers/Directives

  • Use DI in order to access modules
     
  • Use pub/sub communication
     
  • Data and state sharing should be via services

Bonus Tip! Prepare for Angular 2.0 in today's apps

  • Use Component-Based Architecture (a.k.a. use directives, not ng-controllers)
     

  • Reduce dependency on $scope
     

  • Be modular
     

  • Use AngularUI's ui-router
     

  • Don't use angular.element

     

Further reading: 

Part 2

Live Demo

Part 3

Q&A

We're Hiring!