How to not program on AngularJS

limurezzz@gmail.com

Andrei Yemialyanchik

MVC directory structure

templates/ 
    _login.html 
    _feed.html 
app/ 
    app.js 
controllers/ 
    LoginController.js 
    FeedController.js 
directives/ 
    FeedEntryDirective.js 
services/ 
    LoginService.js 
    FeedService.js 
filters/ 
    CapatalizeFilter.js
app/ 
    app.js
Feed/ 
    _feed.html 
    FeedController.js 
    FeedEntryDirective.js 
    FeedService.js 
Login/ 
    _login.html 
    LoginController.js 
    LoginService.js 
Shared/ 
    CapatalizeFilter.js

MVC? MVVM? MVW?

Having said, I'd rather see developers build kick-ass apps that are well-designed and follow separation of concerns, than see them waste time arguing about MV* nonsense. And for this reason, I hereby declare AngularJS to be MVW framework - Model-View-Whatever. Where Whatever stands for "whatever works for you".

Lead of the AngularJS

Global dependencies

var underscore = angular.module('underscore', []); 

underscore.factory('_', function() { 
    return window._; //Underscore must already be loaded on the page 
}); 

var app = angular.module('app', ['underscore']); 
app.controller('MainCtrl', ['$scope', '_', function($scope, _) { 
    init = function() { 
        _.keys($scope); 
    } 
    init(); 
}]);

Like a boss

Scoping $scope's

<div ng-controller="navCtrl"> 
    <span>{{user}}</span> 
    <div ng-controller="loginCtrl"> 
        <span>{{user}}</span> 
        <input ng-model="user"></input> 
    </div> 
</div>
<div ng-controller="navCtrl"> 
    <span>{{user.name}}</span> 
    <div ng-controller="loginCtrl"> 
        <span>{{user.name}}</span> 
        <input ng-model="user.name"></input> 
    </div> 
</div>
app.controller('navCtrl', function($scope) {
  $scope.user = {name: ''};
});

Use modules

var app = angular.module('app',[]);
app.service('MyService', function(){ 
    //service code 
}); 
app.controller('MyCtrl', function($scope, MyService){ 
    //controller code 
});

Small application

var services = angular.module('services',[]); 
services.service('MyService', function(){ 
    //service code 
}); 
var controllers = angular.module('controllers',['services']); 
controllers.controller('MyCtrl', function($scope, MyService){ 
    //controller code 
}); 
var app = angular.module('app',['controllers', 'services']);
var sharedServicesModule = angular.module('sharedServices',[]); 
sharedServices.service('NetworkService', function($http){}); 

var loginModule = angular.module('login',['sharedServices']); 
loginModule.service('loginService', function(NetworkService){}); 
loginModule.controller('loginCtrl', function($scope, loginService){}); 

var app = angular.module('app', ['sharedServices', 'login']);

Group similar types of objects

Group features

Angular way

ANGULAR WAY

We all started with jQuery

Come on, tell me about JQuery

$(document).ready(function () {
    $("#allow_max_members").bind('click', function() {
        var shown;
        shown = $(this).prop('checked');
        $("#max_members").toggle(shown);
    });
});
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title></title>
    <link rel="stylesheet" href="style.css" />
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <div>
        <input type="checkbox" id="allow_max_members" checked='true'/>
        <input id="max_members" type="text" />
    </div>
  </body>
</html>

script.js

Stop using jQuery

In order to really understand how to build an AngularJS application, stop using jQuery. jQuery keeps the developer thinking of existing HTML standards, but as the docs say "AngularJS lets you extend HTML vocabulary for your application."

AngularJS is a framework!

$scope - viewModel

Binding

$scope.$watch('qty * cost', function(newValue, oldValue) {
  //update the DOM with newValue
});

For example, these are valid expressions in Angular:

  • 1+2

  • a+b

  • user.name

  • items[index]

Scope hierarchical structure

$scope

$scope.$digest()

$scope.$apply()

//Pseudo-Code of $apply()
function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

Goes through list of watchers and calls listeners if it's needed starting with the current scope recursively to all descendants.

$digest

appModule.directive("check", function () {
    return function (scope, elem, attrs) {
        elem.bind('click', function () {
            if (elem.prop("checked")) {
                scope.$apply(attrs["myCheck"]);
            }
        });
    };
});

Example: when you need to call $apply manually

Error: $digest already in progress

if(!$scope.$$phase) {
  //$digest or $apply
}

anti-pattern

$timeout(function() {
  $scope.message = "Timeout called!";
})// no need to pass 2nd param if it's zero!

Right way

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 0);

=

$timeout(function() {
    //code here  
}, 100, false); //doesn't call $apply();

Don’t, please don’t use DOM selectors inside controller!

appModule.controller('myCtrl', function($scope) {
    // bad programmer =(
    if ($('.header').length > 0) {
        $('.pointer').css({
                top: - $('.header').height()
            });
    }
})

Controller should not depend on markup!

$ - is jQuery

angular.element('.header')

Error: [jqLite:nosel] Looking up elements via selectors is not supported by jqLite!

Controllers should never do DOM manipulation or hold DOM selectors. Likewise business logic should live in services, not controllers.

native element object

// include BEFORE AngularJS
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.js"></script>
// now AngularJS uses jQuery instead of jqLite
<script src="https://code.angularjs.org/1.2.25/angular.js"></script>

Why not?

  • You can not test such controllers
  • Markup is not reliable
  • Separation of concerns
  • Can't reuse

Why? why?

Because...

interaction problems:

  • communication between scopes

  • communication between DOM elements

  • communication between "components" (whatever it means)

But using "Angular way" you can face...

wait for it

  • Events ($broadcast, $emit)

  • Common scope (be careful with $rootScope)

  • Directives communication ('require')

  • Shared resources (injectable objects: service, factory, value)

Solutions:

If you need DOM element - use directive 

angular.module('appModule')
    .directive("metaTitle", function ($title) {
        return {
            restrict: 'AC',
            link: function (scope, element, attrs) {
                scope.$watch("$current", function(current) {
                    if (current) {
                        $title.documentHeight = element.height();
                    }
                });
            }
        }
    })
    .value('$title', {documentHeight: 50});

Solution examples

Task:

You have fixed header with pointers. Click on pointers should scroll to the desired paragraph on the page.

<body ng-controller='MainCtrl'>
    <div class='header'>
        <ul>
          <li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
            <span ng-hide="$last"> | </span>
          </li>
          <li class='change-height' 
            ng-click='$emit('changeHeight')'>Change height</li>
        </ul>
    </div>
    ...
    <ul>
        <li ng-repeat="item in pointers">
          <a class='pointer' name="{{item}}"></a>
          <span>{{item}}</span>
          ...
        </li>
    </ul>
</body>
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $location, $anchorScroll) {
    $scope.pointers = [1, 2, 3, 4, 5, 6];
    $scope.goToParagraph = function(hash) {
        $location.hash(hash);
        $anchorScroll();
    }
});
.header {
  position: fixed;
  background-color: green;
  width: 100%;
  color: #fff;
}
.pointer{
  position: relative;
  display: block;
  top: -50px;
}
.change-height {
  float: right;
}
app.directive('header', function($rootScope) {
    return {
      link: function(scope, element, attrs) {
        scope.$on('changeHeight', function() {
          var height = Math.floor(Math.random() * 100) + 50;
          element.css({
            height: height + 'px'
          });
          $rootScope.$broadcast('newHeight', height);
        }
      },
      restrict: 'C'
  });
app.directive('pointer', function() {
    return {
      link: function(scope, element, attrs) {
        scope.$on('newHeight', function(e, value) {
          if (value) {
            element.css({'top': -value + 'px'});
          }
        });
      },
      restrict: 'C'
    }
  })

Event communication

Shared resource

app.directive('header', function($header) {
    return {
      link: function(scope, element, attrs) {
        scope.$on('changeHeight', function() {
          var height = Math.floor(Math.random() * 100) + 50;
          element.css({
            height: height + 'px'
          });
          $header.height = height;
        });
      },
      restrict: 'C'
    }
  }
).value('$header', {height: 50});
app.directive('pointer', function($header) {
    return {
      link: function(scope, element, attrs) {
        scope.$header = $header;
        scope.$watch('$header.height', function(newVal) {
            if (newVal) {
                element.css({'top': -newVal + 'px'});
            }
        })
      },
      restrict: 'C'
    }
  })
app.directive('pointer', function($header) {
    return {
      link: function(scope, element, attrs) {
          scope.$watch(function() {
              return $header.height;
          }, function(newVal) {
                if (newVal) {
                    element.css({'top': -newVal + 'px'});
                }
          })
      },
      restrict: 'C'
    }
  })

Common scope

Common scope

app.directive('header', function() {
    return {
      link: function(scope, element, attrs) {
        scope.$on('changeHeight', function() {
          var height = Math.floor(Math.random() * 100) + 50;
          element.css({
            height: height + 'px'
          });
          scope.newHeight = height;
        }
      },
      restrict: 'C'
  });
app.directive('pointer', function() {
    return {
      link: function(scope, element, attrs) {
        scope.$watch('newHeight', function(newVal) {
          if (newVal) {
            element.css({'top': -newVal + 'px'});
          }
        });
      },
      restrict: 'C'
    }
  })
app.directive('header', function() {
    return {
      link: function(scope, element, attrs) {
        scope.$on('changeHeight', function() {
          var height = Math.floor(Math.random() * 100) + 50;
          element.css({
            height: height + 'px'
          });
          scope.internalHeight = height;
        }
      },
      restrict: 'C',
      scope: {
          internalHeight: '=height' // what is the difference between '@', '=', '&'?
      }
  });

Isolated scope

app.directive('pointer', function() {
    return {
      link: function(scope, element, attrs) {
        scope.$watch('offset', function(newVal) {
          if (newVal) {
            element.css({'top': -newVal + 'px'});
          }
        });
      },
      restrict: 'C',
      scope: {
          offset: '='
      }
    }
  })
<body ng-controller='MainCtrl'>
    <div class='header' height='pageScope.newHeight'>
        <ul>
          <li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
            <span ng-hide="$last"> | </span>
          </li>
          <li class='change-height' 
            ng-click='$emit('changeHeight')'>Change height</li>
        </ul>
    </div>
    ...
    <ul>
        <li ng-repeat="item in pointers">
          <a class='pointer' offset='pageScope.newHeight' name="{{item}}"></a>
          <span>{{item}}</span>
          ...
        </li>
    </ul>
</body>
var app = angular.module('app', []);
app.controller('MainCtrl', function($scope, $location, $anchorScroll) {
    $scope.pointers = [1, 2, 3, 4, 5, 6];
    $scope.goToParagraph = function(hash) {
        $location.hash(hash);
        $anchorScroll();
    }
    $scope.pageScope = {newHeight: 50};
});

Change in markup

Directive communication:

how to use 'require'?

<body ng-controller='MainCtrl'>
    <div class='header'>
        <ul>
          <li ng-repeat="item in pointers" ng-click="goToParagraph(item)">Go to {{item}}
            <span ng-hide="$last"> | </span>
          </li>
          <li class='change-height' height='pageScope.newHeight'>Change height</li>
        </ul>
    </div>
    ...
    <ul>
        <li ng-repeat="item in pointers">
          <a class='pointer' offset='pageScope.newHeight' name="{{item}}"></a>
          <span>{{item}}</span>
          ...
        </li>
    </ul>
</body>
app.directive('changeHeight', function() {
    return {
        link: function(scope, element, attrs) {
          element.bind('click', function() {
            var height = Math.floor(Math.random() * 100) + 50;
            element.css({ height: height + 'px' }); // <-- this is not '.header'. It doesn't work!!!
            scope.internalHeight = height;
            scope.$apply();
          });
        },
        restrict: 'C',
        scope: {
            internalHeight: '=height'
        }
    });
app.directive('changeHeight', function() {
    return {
        link: function(scope, element, attrs, ctrl) {
          element.bind('click', function() {
              var height = Math.floor(Math.random() * 100) + 50;
              // we need to define getElement() method in header
              ctrl.getElement().css({ height: height + 'px' }); 
              scope.internalHeight = height;
              scope.$apply();
          });
        },
        restrict: 'C',
        scope: {
            internalHeight: '=height'
        },
        require: '^header' // or '?^header' - doesn't throw error if header is not found
    });
app.directive('header', function() {
    return {
      controller: function($element) {
          this.getElement = function() {
              return $element;
          }
      }
  });

ANOTHER ANGULAR WAY

app.value('$header', {})
  .directive('pointerOffsetCreator', function($header) {
    return {
      restrict: "A",
      scope: {
        className: '@'
      },
      link: function(scope, elem, attrs) {
        if (elem.prop("tagName") == 'STYLE') {
          scope.$watch(function() {
            return $header.height;
          }, function(newValue) {
            function createClassString() {
              return '.' + attrs.className + "{" +
                "top: -" + newValue + "px" + "!important" +
                "}";
            }
            if (newValue) {
              elem.empty();
              elem.html(createClassString());
            }
          });
        }
      }
    }
  })
<head>
    <style pointer-offset-creator class-name='pointer'></style>
</head>

Questions?

I see you want to ask something...

Useful links:

BIG QR=)

Do you want more?

angular.module('app')
.directive('alert', function () {
    return {
        restrict: 'EA',
        controller: ['$scope', '$attrs', function ($scope, $attrs) {
            $scope.closeable = 'close' in $attrs;
        }],
        template: 
"<div class='alert alert-dismissable' ng-class='\"alert-\" + (type || \"warning\")'>\n" +
"    <button ng-show='closeable' type='button' class='close' ng-click='close()'>×</button>\n" +
"    <div ng-transclude></div>\n" +
"</div>\n",
        transclude: true,
        replace: true,
        scope: {
            type: '=',
            close: '&'
        }
    };
})
<html ng-app>
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="style.css" />
    <script src="https://code.angularjs.org/1.3.0-rc.2/angular.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
    <alert type="danger">My message</alert>
  </body>
</html>
// compiled markup
<div class="alert alert-dismissable ng-isolate-scope alert-danger" 
        ng-class="'alert-' + (type || 'warning')" type="'danger'">
    <button ng-show="closeable" type="button" class="close ng-hide" ng-click="close()">×</button>
    <div ng-transclude=""><span class="ng-scope">My message</span></div>
</div>

Alert directive

angular.module('myApp')
.service("$notification", ["$timeout", function ($timeout) {
        var alerts = [];
        var TIMEOUT_MS = 5000;
        this.getAlerts = function() {
            return alerts;
        }
        this.success = function(msg) {
            addAlert(msg, 'success');
        }
        this.error = function(msg) {
            addAlert(msg, 'danger');
        }
        this.warning = function(msg) {
            addAlert(msg, 'warning');
        }
        var addAlert = function (msg, type) {
            if (msg) {
                var item = {msg: msg, type: type};
                alerts.push(item);
                $timeout(function() {
                    var index = alerts.indexOf(item);
                    if (index > -1) {                    
                        alerts.splice(index, 1);
                    }
                }, TIMEOUT_MS);
            }
        };
    }])

Notification service

angular.module('appModule')
.run(function ($templateCache) {
        $templateCache.put("whateverYouWant",
'<alert ng-repeat="alert in alerts" type="alert.type" close="closeAlert($index)">{{alert.msg}}</alert>\n');
     })
.directive("globalNotification", ['$notification', function ($notification) {
        return {
            restrict: 'AC',
            templateUrl: 'whateverYouWant',
            link: function (scope, element, attrs) {
                scope.alerts = $notification.getAlerts();
                scope.closeAlert = function (index) {
                    scope.alerts.splice(index, 1);
                };
            }
        }
        }])

Notification directive

// put this div somewhere on your page
<div class="global-notification"></div>

How to not program on AngularJS

By Andrei Yemialyanchik

How to not program on AngularJS

Before you start programming with AngluarJS please consider typical mistakes and anti-patterns. This presentation will give you a guide to follow best practices and methods. Don't hesitate to use or fork this presentations for you needs. Just keep reference to origin. May the force be with you.

  • 19,378
Loading comments...

More from Andrei Yemialyanchik