Leveling Up – in Angular – Alicia Liu



Leveling Up – in Angular – Alicia Liu

0 16


leveling-up-angular-talk


On Github alicial / leveling-up-angular-talk

Leveling Up

in Angular

http://alicialiu.net/leveling-up-angular-talk

Alicia Liu

@aliciatweet

Sr Software Engineer, Lift

Enjoyment of Angular

AngularRULEZ
I'm doingit wrong!
services,modules
@#$%
promises,directives,...

Module Organization

Module By Feature

Supports asynchronous loading of modules in the future

Services

Features / Recipe type Factory Service Provider can have dependencies yes yes yes uses type friendly injection no yes no object available in config phase no no yes can create functions/primitives yes no yes From Angular Developer Guide

Service vs Factory

var Mario = function() {
  this.size = 'small';
};
Mario.prototype.eatMushroom = function() {
  this.size = 'large';
};
myModule.service('marioService', Mario); // calls new Mario()
myModule.factory('marioService', function() {
  var mario = {
    size: 'small'
  };
  return {
    eatMushroom: function() {
      mario.size = 'large';
    }
  };
});

Provider

myModule.provider('marioService', function() {
  var config = {
    size: 'small'
  };
  return {
    setSize: function(size) {
      config.size = size;
    },
    $get: function(utilities) {
      return {
        eatMushroom: function() { ... },
        // other methods
      }
    }
  };
});

Asynchronous Events

// in some service
$rootScope.$broadcast(MARIO_LOADED, { mario: data}); // when data loads
// in some controller
$scope.$on(MARIO_LOADED, doStuffWithMario);

What happens if the data doesn't load?

Promises   $q

// in some service
var deferred = $q.defer();
svc.getMario = function() {
  return deferred.promise;
};
...
deferred.resolve(data); // when some data loads
...
deferred.reject(error); // if data doesn't load
// in some controller
svc.getMario().then( doStuffWithMario, displayError );

Using Promises

  • Chaining
  • $q.all
  • $q.when
  • Interceptors

Resolve

myModule.config(function($routeProvider) {

    $routeProvider.when('/character/:id', {
      templateUrl: 'profile.html',
      controller: 'CharacterCtrl',
      resolve: {
        character: function($route, CharacterService) {
          // returns a promise
          return CharacterService.get($route.current.params.id);
        }
      }
    });
});
myModule.controller('CharacterCtrl', function($scope, character) {
  $scope.character = character;
});

Directives

Why Write Custom Directives?

  • Reusable Components
  • Manipulate the DOM

Directive Example

Enemy Type:
<enemy enemy-type="{{enemyType}}" lives="lives" on-destroy="destroy()"></enemy>
      
angular.module('demo.characters').directive('enemy', function() {
    return {
        template: "<div ng-class="{goomba: type == \"small\", bowser: type == \"big\"}"></div>",
        replace: true,
        restrict: "E",
        scope: {
            type: "@enemyType",
            currentLives: "=lives",
            onDestroy: "&"
        },
        link: function(scope, iElement, iAttrs) { ... }
    };
});

Restrict

  • "E": Element, <my-directive>
  • "A": Attribute, <div my-directive>
  • "C": Class, <div class="my-directive">
  • Combination e.g. "EA"

Scope

  • false: use existing scope (default)
  • true: new scope
  • object: isolated scope, e.g.
    scope: {
        type: "@enemyType",
        currentLives: "=lives",
        onDestroy: "&"
    }
    • "@": pass as string, can be interpolated
    • "=": data bind this property
    • "&": pass a function

Link: Using $watch

Lives:
link: function(scope, iElement, iAttrs) {
      var $enemy = $(iElement[0]);
      scope.$watch("currentLives", function(newLives, oldLives) {
            if (newLives > oldLives) {
                $enemy.animate({width:"+=10px",height:"+=10px"},150).animate({width:"-=10px",height:"-=10px"},150);
            } else if (newLives < oldLives) {
                $enemy.animate({width:"-=10px",height:"-=10px"},150).animate({width:"+=10px",height:"+=10px"},150);
            }
        });
}

Link: Handle jQuery Event

// jQuery Land
$(document).on('click.fireball', function(e) {
    var $fireball = $("<div class='fire-ball'></div>");
    var offset = $character.offset();
    $fireball.css({top: offset.top + ($character.height()/2), left: offset.left + $character.width()});
    $character.after($fireball);
    $fireball.animate({top: e.pageY, left: e.pageX}, function() {
        $fireball.remove();
        $(e.target).trigger("attack");
    });
});
// in Angular directive
link: function(scope, iElement, iAttrs) {
  var $enemy = $(iElement[0]);

  $enemy.on("attack", function(e) {
      scope.$apply(function() {
          scope.currentLives = scope.currentLives - 1;
          if (scope.currentLives === 0) {
              $enemy.remove();
              scope.onDestroy();
          }
      });
  });
}

DEMO

The EndThanks!

Alicia Liu

@aliciatweet

http://alicialiu.net/leveling-up-angular-talk

Images adapted from Mario Wiki

Bonus Levels

Bad Smells

  • Referencing DOM elements inside controllers
  • Using $parent
  • Over-reliance on $rootScope
  • Over-reliance on $rootScope.$broadcast

Digest Loop Triggers

  • Event handlers
  • Navigation
  • $http
  • $timeout

Compile vs Link

  • No scope
  • Runs once
  • Runs earlier
  • Can return preLink
  • Has scope
  • Runs n times inside ng-repeat

Compile Example

angular.module('static').directive('staticLinky', function ($filter) {
  return {
    restrict: 'A',
    compile: function(tElem) {
      var $elem = $(tElem);
      $elem.html($filter('linky')($elem.text(), "_blank"));
    }
  };
});
<p static-linky>Link this http://example.com</p>

Precompiling Templates

// templates.js
          angular.module('myTemplates', [])
          .run(['$templateCache', function($templateCache) {
          $templateCache.put("template1.html", "<h1>Hello World!</h1>");
          $templateCache.put("template2.html", "...");
          }]);

Rails/Ruby example

// templates.js.erb
angular.module('myTemplates', [])
    .run(['$templateCache', function($templateCache) {
      <% Dir.glob(Rails.root.join('app','assets','templates', '*.html')).each do |f| %>
        $templateCache.put("<%= File.basename(f) %>", <%= File.read(f).to_json %>);
      <% end %>
    }]);

Communicating Between Directives

<mario fire-mode="{{mario.mode}}"></mario>
myModule.directive('mario', function() {
      return {
          restrict: "E",
          scope: {
              fireMode: "@"
          }
      }
  }).
  directive('fireMode', function() {
      return {
          restrict: "A",
          link: function(scope, iElement, iAttrs) {
              var $character = $(iElement[0]);
              iAttrs.$observe('fireMode', function(mode) {
                  if (mode === "fire") {
                      $character.addClass("fire-mode");
                  } else {
                      $character.removeClass("fire-mode");
                  }
              });
          }
      };
  })

Angular sample code

// declare a module
  var myAppModule = angular.module('myApp', []);

  // configure the module
  myAppModule.config(
  //...
  );

  // Add a controller
  function Ctrl($scope) {
  //...
  }

Global variables WTF?!

angular.module('myApp').config(
  //...
  );

  angular.module('myApp').controller('myController', function() {
  //...
  });