Search code examples
angularjsangularjs-service

AngularJS - extending module with own types / providers


I want to ad a new (dialog) type to angular, so I could use it just like I use module.directive, module.filter, module.controller to register directives, filters and controllers.

I want to register my instances of dialog type this way:

module.dialog('prompt',function(dependencies){
    return {
        templateUrl:'prompt.html',
        controller:function($scope){},
        something:'value'
    }
});

I also want to be able to use registered dialogs in controllers (dependency injection)

module.controller('ListCtrl',function($scope,prompt){
    $scope.deleteItem = function(item){
        prompt('Do you want to delete this item?').then(function(result){
            if(result) item.$delete();
        });
    }
});

This boils down to the following questions:

  1. How do I extend angular's module to have module.dialog register my dialog types ?

  2. How do I make my registered dialogs injectable in to controllers etc?

Btw,

  • I know about angular-ui and angular-strap.
  • I would rather not use dialog as a service, but as a separate type (this solution is already implemented in angular-ui).

Solution

  • This is a pretty interesting question. I'll prefix my answer with an opinion: I don't think you should extend angular.module to provide a dialog method. Those methods are shortcuts into built-in Angular providers, and the Angular team adds some from time to time. Since you can get access to the functionality you're looking for without adding the dialog method, I wouldn't. That said, the code below does show you how a very basic version of this might work (it doesn't modify the Angular module prototype, just the single instance of the module).

    <div ng-app="myApp">
      <div ng-controller='MainController'>
        <div>
          <button ng-click='askName()'>Ask Name</button>
          <button ng-click='askNameAgain()'>Ask Name Again</button>
          <button ng-click='askAge()'>Ask Age</button>
          <button ng-click='askFood()'>Ask Food</button>
        </div>
        <div>{{lastResponse}}</div>
      </div>
    </div>
    
    var app = angular.module('myApp', []);
    
    // Provide some basic injectables for testing
    app.constant('nameString', 'NAME');
    app.constant('ageString', 'AGE');
    app.constant('foodString', 'FAVORITE FOOD');
    
    // Create the dialog provider
    app.provider('dialog', function($provide, $injector) {
      var dialogs = {};
    
      this.register = function(name, configFn) {
        // Create a new service
        $provide.factory(name, function($window, $q) {
          dialogs[name] = function() {
            // Get data based on DI injected version of configFn
            var data = $injector.invoke(configFn);
            // faking async here since prompt is really synchronous
            var deferred = $q.defer();
            var response = $window.prompt(data.text);
            deferred.resolve(response);
            return deferred.promise;
          };
          return dialogs[name];
        });
      };
    
      // Injecting the service itself gives you a function that
      // allows you to access a dialog by name, much like $filter
      this.$get = function() {
        return function(name) {
          return dialogs[name];
        };
      };
    });
    
    // Providing dialog injectables via app.config
    app.config(function(dialogProvider) {
      dialogProvider.register('askFood', function(foodString) {
        return { text: 'What is your ' + foodString + '?' }
      });
    });
    
    // Alternatively, shortcut to accessing the dialogProvider via app.dialog
    app.dialog = function(name, configFn) {
      app.config(function(dialogProvider) {
        dialogProvider.register(name, configFn);
      });
    };
    
    app.dialog('askName', function(nameString) {
      return { text: 'What is your ' + nameString + '?' }
    });
    
    app.dialog('askAge', function(ageString) {
      return { text: 'What is your ' + ageString + '?' }
    });
    
    app.controller('MainController', 
                   function($scope, askName, askAge, askFood, dialog) {
      var setLastResponse = function(result) {
        $scope.lastResponse = result;
      };
    
      $scope.askName = function() {
        askName().then(setLastResponse);
      };
    
      $scope.askNameAgain = function() {
        // get the dialog through the dialog service
        // much like how $filter works
        var theDialog = dialog('askName');
        theDialog().then(setLastResponse);
      };
    
      $scope.askAge = function() {
        askAge().then(setLastResponse);
      };
    
      $scope.askFood = function() {
        askFood().then(setLastResponse);
      };
    });
    

    Here is a working example: http://jsfiddle.net/BinaryMuse/zj4Jq/

    By leveraging $injector.invoke inside of your dialogProvider.register function, you can provide the ability to use a key like controller in the data your configFn returns. Since directive works a lot like this already, you may gain a lot from checking out the AngularJS source.