Reputation: 9579
I have a data factory defined in CoffeeScript like:
dataModule.factory "Dates", ->
object:
value: null
and a service defined in CoffeeScript like so:
dataModule.service "DatesService",
["$http", "Dates", "Requesting"
($http, Dates, Requesting) ->
@url = "api/0/dates/"
@object = Dates.object
@getFromServer = (object, onSuccess, onError) ->
Requesting.status = true
$http.get(@url + getCacheBuster()
).success((data, status) ->
data = null unless _.isObject(data)
object.value = data
Requesting.status = false
if onSuccess
onSuccess data
).error (data, status) ->
data = null unless _.isObject(data)
object.value = null
Requesting.status = false
if onError
onError data, status
@get = (onSuccess, onError) ->
if null is @object.value
@getFromServer @object, onSuccess, onError
else
if onSuccess
onSuccess @object.value
@clear = () ->
@object.value = null
]
I then use the service like this:
dataModule.controller "DatesCtrl",
["$scope", "Dates", "DatesService", "Error"
($scope, Dates, DatesService, Error) ->
$scope.dates = Dates.object
$scope.error = Error
$scope.handleError = ->
$scope.dates.value = null
$scope.error.value = "Did not get valid information from server."
$scope.getDates = ->
DatesService.get (data) ->
if not data
$scope.handleError()
return
,
(data, status) ->
$scope.handleError()
return
$scope.getDates()
]
Notice that the service provides a simple caching mechanism. This is so multiple controllers within the same view can get the data from the service, but the service only asks the server once.
Also notice that the injection of Dates, the @url and @value members are the only things that make this specific DatesService not reusable. I would like to abstract this service so that I can "derive"/"extend"/"do other thing" a CachedService into specific "concrete" services that have specific dependency injected, a specific @url and a specific @value.
I have three other services that I want to apply the same pattern to, each with their own data factory and url.
This is probably simple but I am struggling with the right vocabulary to properly search for the answer.
Can someone demonstrate the proper language mechanism(s) to do this abstraction?
Upvotes: 3
Views: 2121
Reputation: 770
I've seen and tried many methods to extend an angular service. The example here - and others which I found work well when extending a service one time. When you want to extend an abstract service multiple times you could use the following approach:
Abstract Service Definition
var AbstractCurdlService = function($q, $http) {
var ENDPOINT = ""; //extend this service
return {
setEndpoint : function(value){
ENDPOINT = value;
},
create: function(payload) {
var deferred = $q.defer();
$http({
method : "POST",
url : this.ENDPOINT,
data: payload
}).then(function(response){
deferred.resolve(response.data);
}, function(error){
deferred.reject(error);
});
return deferred.promise;
},
/* additional service definitions */
};
};
AngularJS Service 1
app.factory('customerService', function($q, $http){
var customerService = new AbstractService($q, $http);
customerService.setEndpoint("/api/customer");
return customerService;
});
AngularJS Service 2
app.factory('reportService', function($q, $http){
var reportService = new AbstractService($q, $http);
reportService.setEndpoint("/api/report");
return reportService;
});
Upvotes: 3
Reputation: 1561
We figured it out, here's a way you can do it that's both simple and clean.
function serviceMaker(service){
return function($injector, /*default injections like*/ $q, $http){
//Add shared functionality here
this.foo = function(){
//to reference dependent variables, I recommend using method access to avoid timing/order issues
return this.MyURL(); //will be declared in extending service
};
//Can use local injections
this.getPromise = $q.when('Definitely using injection too');
//Finishes the other injections
$injector.invoke(service, this);
};
}
angular.module('test', [])
//Then, setup your service like this
.service('GreatService', serviceMaker(function($rootScope){ /*Can inject like normal!*/
//remember to declare dependent methods
this.MyURL= function(){return "http://stackoverflow.com";}
//Can still add your own things
this.bar = function(){
//properties from base service are still accessible
return this.foo()+"?param=huzzah";
}
}))
//Another service
.service('NiceService', serviceMaker(function(){
this.MyURL= function(){return "http://google.com";}
}))
This will do what you're looking for, using angular's $injector
for a clean and straightforward method.
I solved the same problem using the method described below... I'll claim that it works well, but my team's currently investigating some of the deeper Angular functions to see if it can be cleaned up and feel more natural.
Here's the code for your basic service:
var baseService = function(extraInjections, also){
var serviceFunc = function( /*default injections here, like*/ $http, $q, /*and shame*/ param1, param2, param3, param4)
{
//perform other startup stuff set in custom service
also(this, arguments);
//apis can be set here, like
this.getFromURL= function(url){...}
//do common work here, like caching.
//You can reference values that may be set in the custom service,
if(this.url){this.getFromURL(this.url);}
}
//then, inject extra dependencies
var depArray = ['$http', '$q'] //defaults
depArray = depArray.concat(extraInjections);
depArray.push(serviceFunc);
return depArray; //returns service made with dependency array syntax (like ['$q', function($q){}]
}
Then, when writing your services,
myModule.service("ServiceName", baseService([/*any extra injections, like*/'$rootScope'], function(me, injections){
//the injections param holds injections, so here,
var http = injections[0];
//because $http is the first default injection.
//Custom ones can be accessed too
var rootScope = injections[2];
//now, the 'me' variable is your service.
//append api as needed
me.getThatThing = function(){return rootScope.blah};
//make sure you set any expected values you use in your base service
me.url = "http://stackoverflow.com";
}
Obvious flaw that could be improved upon is that in order to add the extra injections, I have to add that silly param1, param2, param3
etc. to my serviceFunc
, so that there's 'room' for the extra custom injections. However, if you're okay with a little clumsiness, I can verify that this works quite well.
Upvotes: 3