1

App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).

The inputs save their data upon blur, so there's no form to submit. That's been working great.

Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.

My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.

Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.

Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.

[edit] a non-functional, simplified look at what I'm doing:

<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">

.directive('saveable', ['savingService', function(savingService) {
return {
    restrict: 'A',
    link: function(scope) {
        scope.saveModel = function(question) {
            savingService.somethingOrOther.save(
                {id: question.id, answer: question.value}, 
                function(response, getResponseHeaders) {
                    // a bunch of post-processing
                }
           );
        }
    }
}
}])
1
  • API logic should be placed in a service, I'm not sure if injecting a service into a directive is the proper Angular way but it would allow you to de-couple the directive from the API and then swap the service being injected for a mocked service or perhaps a different API service altogether, making your directive more reusable. Commented Nov 14, 2013 at 17:06

3 Answers 3

2

No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.

Sign up to request clarification or add additional context in comments.

4 Comments

I am using factory-genearted services, I'm just addressing them through the directive. When you say "a model," could you clarify what you mean? I'm using ng-model obviously
When I say "model", I mean the model in the Model-View-Controller (MVC) perspective. So, a model would be like a Person "class". That way, I can get all people from the server by calling Person.query() and I can create a new person by calling: var person = new Person({first: 'me'}); person.$save(); and I can update the model by calling person.$update(). This is what Angular $resource gives you.
Huh, never occurred to me to extend a resource in that way. Fascinating idea, thank you.
The example I've given, using MongoLab, supports that. I'll modify the sample this weekend to further demonstrate that.
2

The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services

Here is a plunk with code modeled after your own starting example:

Example code:

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

app.service('savingService', function() {
  return {
    somethingOrOther: {
      save: function(obj, callback) {
        console.log('Saved:');
        console.dir(obj);
        callback(obj, {});
      }
    }
  };
});

app.directive('saveable', ['savingService', function(savingService) {
  return {
      restrict: 'A',
      link: function(scope) {
          scope.saveModel = function(question) {
              savingService.somethingOrOther.save(
                  {
                    id: question.id, 
                    answer: question.value
                  }, 
                  function(response, getResponseHeaders) {
                      // a bunch of post-processing
                  }
             );
          }
      }
  };
}]);

app.controller('questionController', ['$scope', function($scope) {
  $scope.question = {
    question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
    id: 1,
    value: ''
  };
}]);

The relevant HTML markup:

<body ng-controller="questionController">
  <h3>Question<h3>
  <h4>{{question.question}}</h4>
  Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>

An alternative using only factory and the existing ngResource service:

However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.

Example using MongoLab collections

I've done something like this to make it easier to use MongoLab collections.

Here's a plunk.

The gist of the idea is this snippet:

  var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
  var apiKey = "YOUR API KEY";

  var collections = [
    "user",
    "question",
    "like"
  ];  

  for(var i = 0; i < collections.length; i++) {
    var collectionName = collections[i];
    app.factory(collectionName, ['$resource', function($resource) {
      var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
      var svc = new resourceConstructor();
      // modify behavior if you want to override defaults
      return svc;
    }]);
  }

Notes:

  • dbUrl and apiKey would be, of course, specific to your own MongoLab info
  • The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
  • There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
  • If you wanted, you could modify the svc instance to vary its behavior by collection type
  • When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
  • This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
  • Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!

Full code:

HTML:

  <body>
    <div ng-controller="questionController">
      <h3>Question<h3>
      <h4>{{question.question}}</h4>
      Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
    </div>

    <div ng-controller="questionController2">
      <h3>Question<h3>
      <h4>{{question.question}}</h4>
      Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
    </div>    
  </body>

JavaScript:

(function() {
  var app = angular.module('savingServiceDemo', ['ngResource']);

  var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;

  function createResource(resourceService, resourcePath, resourceName, apiKey) { 
    numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
    var resource = resourceService(resourcePath + '/' + resourceName + '/:id', 
      {
        apiKey: apiKey
      }, 
      {
        update: 
        {
          method: 'PUT'
        }
      }
    );

    resource.prototype.consoleLog = function (val, cb) {
      console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
      console.log('Logging:');
      console.log(val);
      console.log('this =');
      console.log(this);
      if (cb) {
        cb();
      }
    };

    resource.prototype.update = function (cb) {
      return resource.update({
              id: this._id.$oid
          },
          angular.extend({}, this, {
              _id: undefined
          }), cb);
    };

    resource.prototype.updateSafe = function (patch, cb) {
      resource.get({id:this._id.$oid}, function(obj) {
          for(var prop in patch) {
              obj[prop] = patch[prop];
          }
          obj.update(cb);
      });
    };

    resource.prototype.destroy = function (cb) {
      return resource.remove({
          id: this._id.$oid
      }, cb);
    };

    return resource;  
  }


  var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
  var apiKey = "YOUR API KEY";

  var collections = [
    "user",
    "question",
    "like"
  ];  

  for(var i = 0; i < collections.length; i++) {
    var collectionName = collections[i];
    app.factory(collectionName, ['$resource', function($resource) {
      var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
      var svc = new resourceConstructor();
      // modify behavior if you want to override defaults
      return svc;
    }]);
  }

  app.controller('questionController', ['$scope', 'user', 'question', 'like', 
    function($scope, user, question, like) {
      $scope.question = {
        question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
        id: 1,
        value: ''
      };

      $scope.save = function(obj) {
        question.consoleLog(obj, function() {
          console.log('And, I got called back');
        });
      };
  }]);

  app.controller('questionController2', ['$scope', 'user', 'question', 'like', 
    function($scope, user, question, like) {
      $scope.question = {
        question: 'What is the coolest JS framework of them all?',
        id: 1,
        value: ''
      };

      $scope.save = function(obj) {
        question.consoleLog(obj, function() {
          console.log('You better have said AngularJS');
        });
      };
  }]);  
})();

Comments

0

In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.