AngularJS: $resource with nested resources - angularjs

I am using AngularJS $resource model to REST API. I have got something like this:
angular.module('libraryapp')
.factory('Book', function($resource){
return $resource('books/:id');
});
I am using in these way:
Book.get({ id: 42 }, function(book) {
console.log(book);
});
But I also want an endpoint to a subresource, let's say:
GET /books/:id/comments
How should I define it in module? May I extend Book in some way, to use it like this
Book.get({ id: 42 }).Comment.query(function(comments) {
console.log(comments);
});

You can easily reach nested RESTful resources with AngularJS $resource definitions.
The clue is to understand how the params parameter of each action definition (in the list of actions) in the $resource definition works. As the documentation says, it's an
Optional set of pre-bound parameters for this action. […]
angular.module('libraryApp').factory('Book', [
'$resource', function($resource) {
return $resource('books/:id/:subResource', {}, {
comments: { // The `comments` action definition:
params: {subResource: 'comments'},
method: 'GET'
}
});
}
]);
Given the above definition, you should still be able to use Book as before. For example Book.get({ id: 42 }) translates to a GET books/42/ request.
However, given the new :subResource part of the $resource URL ('books/:id/:subResource'), you now can generate a
GET books/42/comments
request by calling either Book.get({ id: 42, subResource: 'comments' }) or the much more short and elegant interface Book.comments({ id: 42 }) defined as your comments action.

As far as I know, you can't nest resources, but it's pretty simple to do what you're looking for:
You can define optional parameters which you can override in each resource (like category here) or even override the url (look at the otherUrl resource)
angular.module('libraryApp').factory('Book', [
'$resource', function($resource) {
return $resource('books/:id/:category', {}, {
comments: {
method: 'GET',
action: 'category'
},
otherUrls: {
method: 'GET',
url: 'books/:id/admin/:option'
}
});
}
]);

You may want to use Restangular instead as it handles nested resources and a clean and easy way.

As djxak pointed out, adding actions to the resource means that the returned value is the containing resource type, not the sub-resource type.
I solved a similar problem by creating a new resource with the sub-resource URL and modifying the prototype of the containing resource to add a function:
angular.module('libraryapp')
.factory('Book', function($resource){
var bookUrl = 'books/:id',
Book = $resource(bookUrl),
BookComment = $resource(bookUrl + /comments");
Book.prototype.getComments = function () {
return BookComment.query({id: this.id});
};
return $resource('books/:id');
});
The usage then becomes:
Book.get({ id: 42 }).getComments(function(comments) {
console.log(comments);
});
The only downside I see with this approach is that if you have a separate "Comment" resource that is accessed via a different URL, you have to duplicate the $resource initialisation code for the alternative endpoint. This seems a minor inconvenience though.

Related

Populate a list of ngResource from a local variable?

I have a basic ngResource defined per:
angular.module('factories', []).factory('SeedSource', [
'$resource', function($resource) {
return $resource('/seed-sources/:id/', {
id: '#id'
}, {
update: {
method: 'PUT'
}
});
}
]);
And in my Controller I would like to be able to populate a list of this resource from a local variable instead of the traditional .query() method, for instance:
$scope.seed_sources = json_array_of_seed_sources;
This works up until the point I need to call an ngResource method such as:
seed_source.$save()
I know I can go the long way and just add each item to $scope.seed_sources individually each as a new Seeder(...) but I was hoping there might be a cleaner way of achieving this?
If you want to save an array of resources, use the class method:
for (let i=0; i<seed_sources.length; i++) {
SeedSource.save(seed_sources[i], function(source) {
angular.copy(source, seed_sources[i]);
});
};

$resource - proper configuration for ngResource

I have 3 objects in my application, Games, Questions, and Answers.
The classes are configured as such:
class Game{
id;
Question[] questions;
}
class Question{
id;
text;
Answer[] answers;
}
class Answer{
id;
text;
}
I am trying to correctly configure an ngResource to handle this class setup. Ideally, what I'd like to achieve is something like this:
app.factory('gameRepository', function($resource){
var gameResource = $resource('/api/Games/:id', { id: '#id' });
return {
get: function(id){ return gameResource.get({id: id}); }
};
});
app.controller('myController', function(gameRepository){
var game = gameRepository.get(17);
var questions = game.$getQuestions();
var answers = questions[0].$getAnswers();
});
I know that some of this can be achieved by doing this:
var gameResource = $resource('/api/Games/:id/:action', { id: '#id', action: '#action' },
{
getQuestions: { method: 'GET', isArray: true, params: { action: 'Questions'}
},
);
However, I get stuck after this point. Ideally, what I'd like to do is have the $getAnswers method return an object from a different resource (a new questionsResource) and then in turn, have a $getAnswers method that returns from an answers resource. The goal here is to keep the methods on the actual resource object instead of extracting them to a separate factory/service call.
Additionally, I'd like to be able to request a specific question from the repository. Something like this:
var game = gameRepository.get(17);
var question = game.$getQuestion(1);
As far as I can tell, there's no way to pass a specific parameter to a $resource custom action the way I'm using them.
Can anybody point me in the right direction? Thanks!
This ended up being way easier than I thought.
The $resource function creates a function that all objects returned from the resource inherit. As a result you can do something like this:
gameResource.prototype.$getQuestion = function(id){ ... }

How to send objects with arrays using angular resource?

I have a product object with a property called category_ids that is an array of ids.
I've added an $update method to my resource factory so I can send a PUT request.
When I PUT, the server receives data that looks like:
id: 1,
description: 'Yada yada',
category_ids: [1,2,3],
product: { id: 1, description: 'Yada yada' } //<-- I need category_ids in here
How can I get the category_ids array into the product node there?
More detail:
I'm just using angular's resource to update:
'use strict'
angular.module('myApp').factory 'Product', ($resource) ->
resource = $resource '/api/v1/products/:id', { id: '#id' },
update:
method: 'PUT'
return resource
Interestingly, this problem only happens with calling instance methods on my object. Calling the class methods on the factory itself works:
currentProduct.$update() <-- This does not give me the format I want!
Product.update(id: currentProduct.id, product: currentProduct) <-- This does :-\
You can add properties to $resource object like
currentProduct.category_ids = [1,2,3];
Now this property becomes request parameter for PUT request.
Lets suppose you have $resource factory name "Product" then following code will work for you
var currentProduct = new Product()
// Bind properties to resource object
currentProduct.id = 1
currentProduct.description = 'Yada yada'
currentProduct.category_ids = [1,2,3]
// Initiate PUT request.
currentProduct.$update()
I believe Rails expecting put request to follow pattern
/product/:id
so when you want to update product you should call it
product.$update({id:product.id});
and that will make request to url
http://api/v1/products?id=1
with request payload like
{"id":"1","description":"Yada yada","category_ids":[1,2,3]}
please see demo here
http://plnkr.co/edit/Utjoj6LirxvzMGSwoffo?p=preview
We would need the code of that $update method, but I suspect it is relying on the this keyword, which is different depending on which object you are calling the function from.
Could you please try this and let us know if it works:
currentProduct.$update.bind(Product) ();
If it does, this means that indeed $update expect this to be Product (which is not the case and is currentProduct in your example instead).
Create a angular service
// Angular Service
angular.module('myApp').factory('Product', function($resource,ENV)
return $resource(ENV.apiEndpoint+'/api/v1/products/:id', {}, {
update: { method: 'PUT' , params:{id:'#id'}}
});
});
In your controller you have to set product object
angular.module('myApp').controller('ProductController',
function ($scope,Product) {
var requestParams = {
id: 1, //Service extracts this parameter to form the url
product: {
id: 1,
description: 'Yada yada'
category_ids: [1,2,3]
}
}
Product.update(requestParams,function(success){
console.log(success)
},function(error){
console.log(error)});
});

AngularJS REST service calls

I'm looking to add factory/service calls for the URLs below to an AngularJS project in a way that follows the DRY principle. The project uses ngResource.
http://localhost/vehicles/{type:car|truck}/drive/{2wd|4wd}?sort={"start":"1", "limit':"10", "sortBy": "make"}
http://localhost/vehicles/{type:car|truck}/drive/{2wd|4wd}/count
http://localhost/vehicles/bestselling/{type:car|truck}?sort={"start":"1", "limit':"10", "sortBy": "make"}
http://localhost/vehicles/bestselling/{type:car|truck}/count
All calls are HTTP GET
The URL path parameters "{type:car|truck}" can be either one of "car" or "truck" the same goes for "{2wd|4wd}".
The URLs ending with count return the number of items (for pagination); the rest return the list of items to be displayed.
How can I define the factory/service calls for these resources in Angular? I have not had any luck finding an answer for this; the closest I've found is this
Disclaimer: I have no experience with AngularJS
My solution:
services.js
resources.factory('Constants', [
function() {
return {
RESOURCE_URL: "http://localhost/vehicles"
}
}
]);
resources.factory('Rest', ['Constants', '$resource', function(C, $resource) {
return {
BestSellingCount: $resource(C.RESOURCE_URL + '/bestselling/:type/count', {
type: '#type'},{})
BestSelling: $resource(C.RESOURCE_URL + '/bestselling/:type', {
type: '#type'}, {
getBestSelling: { method:'GET', params: {sort: {"start":"1", "limit':"10", "sortBy": "make"}}, isArray: true)}}})
}]);
And in my controller
$scope.bestsellingtrucks = Rest.BestSelling.getBestSelling({type:"truck"});
The default sort and pagination values get passed in as a query string

AngularJS Resource won't work with an array

I am trying to use AngularJS to retrieve a JSON array (a list of strings). I have created a Resource, like so:
packageResource.factory('Package', ['$resource', function($resource) {
return $resource('/static/package.json', {}, {
'get': {
method: 'GET',
transformResponse: function (data) {
return angular.fromJson(data)
},
isArray: true
}
});
}]);
And my Controller like this:
MainController = [
'$scope', 'Package', function($scope, Saved, Rules) {
$scope.package = (Package.get());
}
];
And my template:
<ul>
<li ng-repeat="item in package">{{item}}</li>
</ul>
<p> {{package}}</p>
I expect it to display a list of all the items in the package.json array, and then, for testing, I added the {{package}} and would expect it to print the contents, but instead, I get:
{}
{}
{}
{}
{"0":"s","1":"r","2":"d"}
The JSON file contains the following:
[1,3,6,"srd"]
However, if I change my package.json to an object, it works perfectly. For example:
{
"author": "John",
"name": "project",
"version": 1
}
And of course, change isArray to false. I get:
John
project
1
So it appears that something can't handle an array, and mangles it into an object. I am having trouble figuring out what - all the Angular documentation shows Resource and Scope working with arrays, and it doesn't make since for them not to. I explicitly added the isArray: true flag to my Resource definition, but no luck.
I am using Angular version 1.0.8. I am a complete newbie, so it may be something painfully obvious, but I have been trying to get this to work for over a day now.
isArray=true means your json data looks like an array of objects, aka, a list. Each item in the array must be an object. so your json data needs to be [{name:value,name2:value2},{etc},{etc}].
isArray=false means your json data is a singular object.
This is the standard way of representing the restful data that $resource was designed to work with.

Resources