Nested data/children with Angular's factory $resource - angularjs

I have a server side API method which returns an array of locations in JSON and have created a resource in Angular to retrieve the entire array of locations upon loading.
All locations also have infinite nested/children locations which is contained within an array inside of the 'children' property for each location. Here's an example to demonstrate what the server response may look like:
[
{
id: 1,
name: 'Top Level',
children:
[
{
id: 2,
name: 'Second Level',
children: [ ... ]
}
]
}
]
The $save() method works correctly when calling it on top level nodes, as expected (although the payload also includes the nested children property when executing the POST).
This however does not work when attempting to call $save() on any of the nested 'children' nodes. This leads me to the conclusion that the resource methods are not attached in a recursive manner; but is there a way in which one may specify a recursive or nested property in a resource (I've found nothing in the official angular documentation along these lines) so that I may attach the $save() method to nested children nodes as well?
Here's my existing code for reference:
angular.module('locations', [ 'ngResource' ])
.factory('Locations', function( $resource )
{
return $resource(
'./locations', {},
{ query: { method: 'GET', isArray: true } }
);
});

Related

AngularJS with Hypermedia (HATEOAS): how to use hypermedia urls accross states

I have an AngularJS application with ui router that consumes a REST API with Hypermedia. The general idea is to have the API generate the urls for its various calls, and keep the client from constructing the urls itself.
For example, when fetching the list of products, here's what the API returns:
[
{
"id": 1,
"name": "Product A",
"_links": {
"self": {
"href": "http://localhost:4444/api/products/1",
"name": null,
"templated": false
},
"actions": []
}
},
{
"id": 2,
"name": "Product B",
"_links": {
"self": {
"href": "http://localhost:4444/api/products/2",
"name": null,
"templated": false
},
"actions": []
}
}
]
So, the problem: I want to navigate to the detail of a product. I have the API url available from the collection hypermedia. However, upon changing states, I somehow need to pass this url to the detail state, so that the controller of the detail state can fetch the product.
The UI urls are completely decoupled from the API urls, i.e. the client has its own url scheme.
What's the best way to achieve this, all the while keeping the client stateless and each page bookmarkable?
One solution is to pass the url by ui router's data property. However, the page wouldn't be bookmarkable. Another way is to pass the API url in the UI url, which would keep the page bookmarkable (as long as the API url doesn't change), but the UI url would be very ugly.
Any other thoughts?
Unless I'm very wrong about this, I'm not looking for a templated solution, i.e. a solution where the API returns a template of a url that needs to be injected with parameters by the client. The whole point is that the url is already populated with data, as some urls are quite a bit more complicated than the example provided above.
I've encountered this problem a few times before. I've detailed my preferred solution step-by-step below. The last two steps are specifically for your problem outlined in your post, but the same principle can be applied throughout your application(s).
1. Root endpoint
Start by defining a root endpoint on the API level. The corresponding root entity is a collection of top level links, in other words links to which the client(s) require direct access.
The idea is that the client only needs to know about one endpoint, namely the root endpoint. This has the advantages that you're not copying routing logic to the client and that versioning of the API becomes a lot easier.
Based on your example, this root endpoint could look like:
{
"_links": {
"products": {
"href": "http://localhost:4444/api/products",
}
}
}
2. Abstract main state
Next define an abstract super state that resides at the top of your state hierarchy. I usually name this state main to avoid confusion with the root endpoint. The task of this super state is to resolve the root endpoint, like:
$stateProvider.state('main', {
resolve: {
root: function($http) {
return $http.get("http://localhost:4444/api/").then(function(resp){
return resp.data;
});
}
}
});
3. Products overview state as child of the main state
Then create a products state which is a descendant from the main state. Because the root endpoint is already resolved, we can use it in this state to get the link to the products API:
$stateProvider.state('products', {
parent: 'main',
url: "/products",
resolve: {
products: function($http, root) {
return $http.get(root._links.products.href).then(function(resp){
return resp.data;
});
}
}
});
4. Product detail state as child of the products state
Lastly, create a product detail state as a child of the products state above. Pass in the product's id via the $stateParams (hence, it's part of the client URI) and use it to retrieve the correct product from the products array resolved in the parent:
$stateProvider.state('products.product', {
url: "/{productId}"
resolve: {
product: function($http, $timeout, $state, $stateParams, $q products) {
var productId = parseInt($stateParams.productId);
var product;
for (var i = 0; i < products.length; i++) {
product = products[i];
if (product.id === productId) {
return $http.get(product._links.self.href).then(function(response){
return response.data;
});
}
}
// go back to parent state if no child was found, do so in a $timeout to prevent infinite state reload
$timeout(function(){
$state.go('^', $stateParams);
});
// reject the resolve
return $q.reject('No product with id ' + productId);
}
});
You can move the code above into a service to make your states more lightweight.
Hope this helps!

Access to nested object in JSON ,select the right value in a dropdownlist (Angularjs)

I am a beginner in AngularJS. I used the $resource service to get a specific object of type person based on its id from the server in order to update this object.
Resource service:
moduleName.factory('Person', function ($resource) {
return $resource('/application/person/:id', {}, {
show: { method: 'GET' , params: {id: '#id'}},
});
});
The object that I received and it displayed in my browser using the {{}} is:
{
"id": "560cf96ee85532035928889e",
"firstName": "Christiane",
"gender": "Male",
"flight": {
"id": null,
"arrivalDate": "2015-01-05 11:30",
"arrivalAirport": {
"name": "Frankfurt"
},
"stopovers": []
}
}
In this form I have a dropdownlist that contains the Airport list, I expect that the value of the arrivalAirport will be selected in this dropdown list, but no value was selected, I try to set the selected value of this dropdown list in the AngularJs controller of this view.Before setting the value of this object I try to access to their value by this code inside the controller.
moduleName.controller('PersonDetailController',function ($scope, $routeParams, Person,$location) {
$scope.person = Person.show({id: $routeParams.id});
console.log($scope.person.flight.arrivalAirport);
});
when I want to get the value of the object flight.arrivalAirport nested in person object I got this error.
TypeError: Cannot read property 'arrivalAirport' of undefined
How can I display the right value selected in the dropdown list and how can I access to the other object like stopovers array in order to update their values?
Thank you.
When you call console.log Person's data hasn't been retrieved yet. You should do something like this:
$scope.person = Person.show({id: $routeParams.id}, function(person) {
console.log(person.flight.arrivalAirport);
});
In general, any code working on data received asynchronously can't be put right after the call because it's very unlikely that data has been retrieved at that point, you should use callbacks and promises.
Your view should update automatically, and you said it does, at least for the part showing the json. In order to make the dropdown update you should bind its values to a property inside $scope.person
If the dropdown doesn't update then there must be some other error and you might need to post the dropdown's code too.
If you want to manually update the dropdown's list instead then you need to put the relevant code inside the callback (the function i added to "person.show" in the code above) in order to make it work

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: $resource with nested resources

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.

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