Decoupling AngularJs Using Providers

About Tyler Jones

Software Engineer at

building applications with

We're Hiring

$injector

invoke(fn)

Invoke the method and supply the method arguments from the $injector.

instantiate(Type)

Create a new instance of JS type. The method takes a constructor function, invokes the new operator, and supplies all of the arguments to the constructor function as specified by the constructor annotation.

Factory

  • A component
  • Defined by a function
  • Invoked by the $injector service
  • Return value of invoke becomes the component

Demo

Write a factory

Service

  • A component
  • Defined by a function
  • Instantiated by the $injector service
  • Return value of instantiate becomes the component

Factory vs. Service

  • Both components
  • Both defined by a function
  • Both injected with the $injector service
  • One is invoked other is instantiated, service is called with new
  • Return value become the component


Use a Factory to define a type that will be called with new

Use a Service to create an object that will be shared across all modules and components (singleton)

Demo

Write a service

Provider

  • A component
  • Defined by a function
  • Invoked with the $provider
  • Required to have a $get property
  • $get is invoked with the $injector service

Demo

Write a provider

"use the source" + luke;

What is really going on here?

var providerCache = {};

function provider(name, provider_) {
  if (isFunction(provider_) || isArray(provider_)) {
    provider_ = providerInjector.instantiate(provider_);
  }
  if (!provider_.$get) {
    throw "Provider '" + name + "' must define $get factory method.", name);
  }
  return providerCache[name + providerSuffix] = provider_;
}
function factory(name, factoryFn) {
  return provider(name, {
    $get: factoryFn
  });
}
function service(name, constructor) {
  return factory(name, ['$injector', function($injector) {
    return $injector.instantiate(constructor);
  }]);
}

Latest Angular Source

How this works

  1. $provider created with component "$provider", which is itself
  2. $injector created with component "$injector", which is itself
  3. All components registered with module.provider are added to $provider
  4. Angular runs all blocks registered with module.config
  5. $get property of components in the $provider are added to the $injector
  6. Angular runs all blocks registered with module.run

Why do we want $get and providers?

  • To be able to configure our services at run time
  • Inject settings and configuration between modules
$get

Demo

Write a useful provider

Angular Router

angular.module('ngRoute', []).

provider('$route', [function () {
  var routeCache = {};

  return {
    when: function (url_template, options) {
        routeCache[url_template] = options;
    },
    $get: ['...', function (...) { ... }
  };
}]);
angular.module('myModule', ['ngRoute']).

config(['$routeProvider', function ($routeProvider) {
  $routeProvider.when('/my-route/:some_param', {
    controller: 'myController as my_controller',
    templateUrl: 'my_template.html',
    resolve: {
      dependency: ['$route', function ($route) {...}]
    }
  });
}]);

Key and Data Injection

Problem:

  • Setting API keys in each environment while keeping the application generic
  • Adding data to the page on initial request the server

Requirements:

  • Keeping JS builds generic, by loading keys based on environment 
  • Reducing number of requests, to improve preformance
angular.module('spacelist').

config(['webServiceKeysProvider', function (webServiceKeysProvider) {
  webServiceKeysProvider.
    setGoogleAnalyticsKey('<%= GA_KEY =>').
    setKissmetricsId('<%= KM_ID =>').
    setGoogleMapsKey('<%= GMAPS_KEY =>').
    setStripePublicKey('<%= STRIPE_PUB_KEY =>');
}]).

config(['userManagerProvider', function (userManagerProvider) {
  userManagerProvider.inject_current_user(<%= current_user.as_json() =>);
}]).

config(['appManagerProvider', function (appManagerProvider) {
  appManagerProvider.inject_app_data(<%= app_data.to_json() %>);
}]);
angular.module('webServices')

.service('googleMapsLoader', [
  '$window', 'ScriptInjector', 'webServiceKeys', function
  ($window,   ScriptInjector,   webServiceKeys) {
    var maps_callback_name = 'google_maps_ready';

    this.load = function (callback) {
      if (typeof $window.google === 'object') {
        callback($window.google);
      } else {
        $window[maps_callback_name] = function () {
          callback($window.google);
        }
        new ScriptInjector(
            '//maps.googleapis.com/maps/api/js?v=3.18' +
              '&key=' + webServiceKeys.getGoogleMapsKey() +
              '&callback=' + maps_callback_name).
          inject();
      }
    };
}]);

Modals

Problem:

  • Need to show modal content to user depending on various user actions and events

Requirements:

  • Reusable across the entire site
  • Have a consistent UI, without code duplication
  • Data can be passed to modals to modify the behaviour
modalProvider.add('loginSignup', {
  controller: 'LoginSignupController as login_signup_ctrl',
  templateUrl: 'angular-app/auth/login_signup.html',
});

...

modalProvider.add('purchaseBCA', {
  controller: 'BCAPurchaseController as bca_purchase_ctrl',
  templateUrl: 'angular-app/bca/modals/bca_purchase.html',
});
modal.show('loginSignup', next: '/privileged_url/');

...

modal.show('purchaseBCA', {property: property, bca: bca});
"use strict";

angular.module('spacelist.core').

provider('modal', [function () {
  var modalProvider = {},
      modals = {};

  modalProvider.add = function (name, options) {
    modals[name] = _.extend({
      preventClose: false,
      locals: {},
    }, options);
    return this;
  };

  modalProvider.$get = [...];

  return modalProvider;
}]);
[..., function ($rootScope, $controller, $compile, $window, loadTemplate) {
  return {
    show: function (name, locals) {
      var modal_options = modals[name],
          modal_element = angular.element(
           '<div class="modal modal-show">' +
           '<div class="modal-wrapper"><div class="modal-content"></div></div>' +
           '<div class="modal-cover"></div></div>'),
          content_element = modal_element.find('.modal-content'),
          modalController = {
            show: function () { angular.element('body').append(modal_element); },
            close: function () { modal_element.remove(); }
          },
          controller_locals = angular.extend({}, modal_options.locals, locals, {
            $scope: $rootScope.$new(true),
            modalController: modalController
          });

      $controller(modal_options.controller, controller_locals);

      content_element.html(loadTemplate(modal_options.templateUrl));
      var link = $compile(content_element.contents());
      link(controller_locals.$scope);

      modalController.show();

      return modalController;
    }};
}}];
var cover_element = modal_element.find('.modal-cover')

if (!modal_options.preventClose) {
  cover_element.on('click', modalController.close);
  $window.addEventListener('keydown', escHandler);
}

function escHandler(event) {
  if (event.keyCode === 27) {
    modalController.close();
    $window.removeEventListener('keydown', escHandler);
  }
}

Modals that can be closed

Polymorphic UI Element Injection

Problem:

  • Different UI component to be shown based on object

Requirements

  • Module should not contain all the components for each UI element
  • Reuse components from angular routes, specifically controllers and services
angular.module('spacelist.profile', ['spacelist.conversations'])

.config([
  'conversableTypeProvider', function
  (conversableTypeProvider) {
    conversableTypeProvider.register('Profile', {
      controller: 'profileController as profile_ctrl',
      templateUrl: 'angular-app/profile/profile_conversable.html',
      resolve: {
        user: ['params', 'User', function (params, User) {
          return User.query({profile_id: params.conversable_id}).
            $promise.then(function (users) {
              if (users.length === 0) {
                throw "Unknown user";
              }
              return users[0];
            });
        }]
      },
    });
}]);

Adding Element to a Page

<conversable
    conversable-type="conversable.type"
    conversable-id="conversable.id">
</conversable>
angular.module('spacelist.conversations').

directive('conversable', [
 '$compile', '$controller', 'conversableType', function 
 ($compile,   $controller,   conversableType) {
  return {
    scope: {
      type: '=conversableType',
      id: '=conversableId'
    },
    link: function (scope, element) {
      scope.$watch('type + id', function () {
        if (scope.type && scope.id) {
          build_element();
        } else {
          element.empty();
        }
      });
    }

    function build_element() { ... }
  };
}]);
function build_element() {
  var conversable_component = 
          conversableType.getComponent(scope.type),
      child_scope = scope.$new(true);

  element.addClass('loading');
  element.empty();

  conversable_component.resolveLocalsFor(scope.id).
    then(function (locals) {
      var controller = conversable_component.getController();

      element.html(locals.template);

      var link = $compile(element.contents());

      locals.$scope = child_scope;
      $controller(controller, locals);

      link(child_scope);
    }).
    catch(show_error).
    finally(element.removeClass.bind(element, 'loading'));
}

Questions?

 

Tyler Jones <tyler@squirly.ca>

GitHub: @squirly

Is Hiring!