I was going through the angularJS $Resource service documentation and I came across something strange.
(https://docs.angularjs.org/api/ngResource/service/$resource)
There are some custom action methods and you can also define your own. Then, these methods can be called with the following parameters:
HTTP GET "class" actions: Resource.action([parameters], [success], [error])
non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
But then, later in the example, a default action method:
'query': {method:'GET', isArray:true}
is called like this:
var CreditCard = $resource('/user/:userId/card/:cardId',
{userId:123, cardId:'#id'}, {
charge: {method:'POST', params:{charge:true}}
});
var cards = CreditCard.query(function() {
// GET: /user/123/card
// server returns: [ {id:456, number:'1234', name:'Smith'} ];
var card = cards[0];
...
});
As you can see, the first parameter of the query method is the success callback function (at least, that's what I'm assuming this is) instead of the [parameters] param.
I'd expect:
var cards = CreditCard.query({}, function() { ... });
Am I missing something or can you simply omit the first parameter if you don't need it?
I know this is kind of a silly question but I want to make sure I'm not getting it wrong here...
The [] bracket in the Resource.action([parameters], [success], [error]) mean that those parameters are optional and could be omitted.
But that doesn't tell what will happen if only a single callback is passed as a parameters and you would think:
Will it be considered as a [parameters] or [success] or [error], since those are all optional, not even count the postData for a non-GET action.
I'm not sure if this is mentioned anywhere in the documentation. But to clear things up, lets explore the angularjs source code and you will find the exact logic for that.
https://github.com/angular/angular.js/blob/v1.2.21/src/ngResource/resource.js#L470
And here are all the combination that valid for a GET request.
// 4 arguments
params, data, success(), error()
// 3 arguments
params, data, success()
params, success(), error()
// 2 arguments
params, success()
success(), error()
// 1 argument
success()
params
Yes you can omit the first parameter if you don't want to pass any parameters with your request. As stated in the documentation:
The action methods on the class object or instance object can be
invoked with the following parameters:
HTTP GET "class" actions: Resource.action([parameters], [success],
[error])
The [] bracket notation implies that it is optional, so if "parameters" is not provided then the value expected are success and error callbacks.
Related
I am facing a very weird case in my angularjs app. In a factory, the following code works properly:
$http.put(apiBase + 'delete?id='+orderId);
Which obviously connects to an api to perform a PUT operation (it is called "delete" here but it actually only updates a flag in the record).
But the same code, when written this way, does not work:
$http.put(apiBase + 'delete', {
params: {
id: orderId
}
}
);
Which is funny, because I am using the exact same syntax in some other factories to hit similar APIs and they work!
I believe this is because the second argument of $http.put() is a data object, not a config object. You are passing a config object as the second parameter.
You could try:
$http.put(apiBase + 'delete', null, {
params: {
id: orderId
}
}
);
https://docs.angularjs.org/api/ng/service/$http#put
When using $http.put, you don't need to wrap your data in the config object. You can pass the data object directly, and then omit the third parameter:
$http.put(apiBase + 'delete', { id: orderId });
Your other factories probably work with the syntax stated in your question because you are making $http.get or $http.delete requests.
I have found that this slightly-different API for the various "shortcut" methods to be confusing enough that I almost think it's better to avoid them altogether. You can see the differences from the documentation where get and delete have two parameters:
get(url, [config]);
delete(url, [config]);
and most of the other shortcut methods have three:
post(url, data, [config]);
put(url, data, [config]);
Note that the [config] object is defined further up on that documentation page, which is where it defines that "params" property:
params – {Object.} – Map of strings or objects which
will be serialized with the paramSerializer and appended as GET
parameters.
I made a base class, HttpService:
class HttpService
constructor: (url) ->
#service = $resource url, {}, #actionOptions()
actionOptions: ->
query:
method: 'GET'
isArray: true
transformResponse: (data) =>
response = []
wrapped = angular.fromJson(data)
angular.forEach wrapped[#indexRoot], (item) =>
response.push #jsonToModel(item)
response
I inherit like this:
class FooService extends HttpService
constructor: ->
super '/api/foos/:id'
#indexRoot = 'foos'
all: ->
#service.query()
jsonToModel: (data) ->
new Foo(data)
In general, the idea is to extend HttpService providing the URL of the resource and the index root (JSON has a root element for the index action). I override jsonToModel to provide a way to transform each JSON element in the response into a custom object, in this case Foo.
When debugging, I can see that response is what it should be in actionOptions(), so it seems like transformResponse is returning what I expect, an array of Foos.
Whenever I call FooService.all() though, I get an array of Resources (ngResource default)... anyone have any idea what I'm doing wrong? I've omitted the dependency injection and everything, but it's all working and I have no errors in console.
The $resource service wraps the response in Resource objects using an interceptor, which occurs after the transformResponse, so while transformResponse is a good place to unwrap a root object (the results from there get fed into the interceptors), you will also need an interceptor to call your jsonToModel function.
The relevant docs:
https://docs.angularjs.org/api/ngResource/service/$resource
transformResponse – {function(data, headersGetter)|Array.} – transform function or an array of such functions. The transform function takes the http response body and headers and returns its transformed (typically deserialized) version. By default, transformResponse will contain one function that checks if the response looks like a JSON string and deserializes it using angular.fromJson. To prevent this behavior, set transformResponse to an empty array: transformResponse: []
interceptor - {Object=} - The interceptor object has two optional methods - response and responseError. Both response and responseError interceptors get called with http response object. See $http interceptors.
In ngResource action I can specify custom request headers. However I need to set the headers at the time of calling the resource action.
The reason is I need paging and sorting data for the list query, and those need to be specified by custom headers (X-Order, X-Offset, and so on). This data can vary from call to call, so I cannot have them in the resource action definition.
Is there a way to pass headers while calling the action? (other than setting $http defaults)
Try Restangular service.
You can find there method: setFullRequestInterceptor which may fit your needs
//From Documentation
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
return {
element: element,
params: _.extend(params, {single: true}),
headers: headers,
httpConfig: httpConfig
};
});
Examples: http://plnkr.co/edit/d6yDka?p=preview
Say, I have the following resource:
angular.module('app.resources.demo-resource', ['ngResource'])
.service('demoResource',
function ($resource) {
return $resource('/api/path', { }, { put: { method: 'PUT' } });
});
and I want to invoke the put request, using promises later.
How do I do it correctly?
newResource.$put().then(...) or newResource.put().then(...)?
Does both variants exist and what is the difference between them?
When you define a "non-GET" action (basically, one with a method that has a request body: PUT, POST, PATCH), ngResource will automatically create corresponding actions/methods on every instance of that resource (prefixed by $). This is just for convenience.
Basically, you have two ways to call such an action:
<Resource>.action([parameters], postData, [success], [error])
or
<instance>.$action([parameters], [success], [error])
The benefit of the latter is that you don't need to specify the postData (it's the <instance> object that acts as the data).
With promises: (To the answer of #ExpertSystem)
<Resource>.action([parameters], postData).$promise
or
<instance>.$action([parameters])
is automatically a promise. The first approach looks more stable in practice. (Angular resolved an instance to the wrong resource after I did _.extend({}, resource))
In the "wire up a backend" demo code from the angularjs site they set up a db call. From what I can tell they're extending the update function in order to add some extra parameters needed by the mongolab api.
angular.module('mongolab', ['ngResource']).
factory('Project', function($resource) {
var Project = $resource('https://api.mongolab.com/api/1/databases' +
'/angularjs/collections/projects/:id',
{ apiKey: '4f847ad3e4b08a2eed5f3b54' }, {
update: { method: 'PUT' }
}
);
Project.prototype.update = function(cb) {
return Project.update({id: this._id.$oid},
angular.extend({}, this, {_id:undefined}), cb);
};
Then they call the update property like this:
$scope.save = function() {
$scope.project.update(function() {
$location.path('/');
});
I've tried using this code to build a demo app using a local development server so I've omitted extending the update property as I don't need the extra $oid parameter. What I do need is to specify that the update method should use PUT. My code is like this:
var Unit = $resource('http:/localhost/api/unit/:id', {id:'#Unit_Id'},
{'update': { method: 'PUT' }});
And then calling it like this:
$scope.save = function () {
$scope.unit.update(function () {
$location.path('/unitlist');
});
But What I've discovered is that the code only runs with a dollar sign in front of update like this:
$scope.save = function () {
$scope.unit.$update(function () {
$location.path('/unitlist');
});
So here are my questions:
In the demo code, where is "update" actually added to the Project variable? As a parameter in $resource or using prototype to extend Project?
Why is update undefined in my code unless I prefix $ when I call it?
"In the demo code, where is "update" actually added to the Project
variable? As a parameter in $resource or using prototype to extend
Project?"
Adding the following code to end of the factory() method:
console.log('Project.update=' + Project.update)
console.log('Project.prototype.update=' + Project.prototype.update)
return Project;
we see that Project.update is a function, and that Project.prototype.update is another/different function. So there are actually two update functions. Function Project.update is a (non-GET) "class" action, so it is called without a $ prefix. update() is a function defined on the object returned by $resource -- i.e., Project. Indeed, we see that function Project.prototype.update calls Project.update(...).
In the EditCtrl, when data is returned from the server, the project variable is a resource "instance" object (which is different from the resource "class" object), hence it has (non-GET) "instance" actions, which are prefixed with $. One of those actions is $update. Add the following log() to the EditCtrl function to prove that project.$update() exists:
Project.get({id: $routeParams.projectId}, function(project) {
console.log('project.$update=' + project.$update)
So now we have a third "update" function. However, in the demo code, they don't use this project.$update() function -- they use the function they defined on Project.prototype instead.
"Why is update undefined in my code unless I prefix $ when I call it?"
In your code, Unit.update should be defined (this is a resource "class" action), and when your data is returned from the server, then $scope.unit.$update becomes defined (this is a resource "instance" action). Unit is a class, $scope.unit is an instance.
In your code, you probably don't need to use Unit.update because I'm guessing you first Unit.get() your data, and then you assign the returned data to $scope.unit, and then you update it using the instance (not the class), hence you are updating with $scope.unit.$update() and not Unit.update(), which is the recommended approach.
Ok, here it is from the docs
The action methods on the class object or instance object can be
invoked with the following parameters: HTTP GET "class" actions:
Resource.action([parameters], [success], [error]) non-GET "class"
actions: Resource.action([parameters], postData, [success], [error])
non-GET instance actions: instance.$action([parameters], [success],
[error])
So, when you when you extend the class (prototype), call it without the $ like normal, but when you add an action as a parameter of $resource prefix $.
As far as I get it they extend the prototype with this piece of code:
Project.prototype.update = function(cb) {
return Project.update({id: this._id.$oid}, angular.extend({}, this, {_id:undefined}), cb);
};
Then they create an instance of the Project in line 30 of the project.js
$scope.project = new Project(self.original);
And then they can perform an update by simply calling:
$scope.project.update(...)
If you got rid of extending the prototype of your service, you can't call $scope.unit.update because there exist no property of unit that is called update.
I hope it helps :)