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

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!

Related

how to refresh data in cache on state reload- angular 1.5 / ui-router

I am working on a task where I have a bunch of widgets on a dashboard. When the user changes the customer on the dashboard the $state is reloaded, and the widgets' position should be saved to a cache, but the data relevant to the widgets should be refreshed. There are two relevant components, a header component where the customer is changed, and the dashboard component where the widgets are. I'd like to accomplish this without touching the header component.
Previously, I wrote a function to remove all data related properties from the cache after saving in the cache, but I think that might not be the right approach and too complicated. what is the best way to accomplish this?
so suppose each widget looks like this:
{
widgetType: chart,
position: someCoordinatesThatStayTheSameOnStateChange,
chart: containsDataThatNeedsToBeRefreshed,
chartData: containsDataThatNeedsToBeRefreshed
}
this is a $resolve'd function in my routes file which exposes $ctrl.widgets in the dashboard controller:
widgets($http, $q, CacheFactory, WidgetsService) {
'ngInject';
let mockWidgetData = WidgetsService.getAllWidgets();
let widgetsCache = CacheFactory.get('widgets');
if (!widgetsCache) {
CacheFactory.createCache('widgets', {
storageMode: 'localStorage',
});
}
return mockWidgetData;
},
relevant places where I save data in a cache in the dashboard controller:
let widgetsCache = CacheFactory.get('widgets');
let widgetsCacheItems = widgetsCache.get('widgets');
gridsterConfig.resizable.stop = function(event, $element, widget) {
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
gridsterConfig.draggable.stop = function(event, $element, widget) {
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
$ctrl.toggleVisibility = function(widget) {
widget.hidden = !widget.hidden;
widgetsCache.put("widgets", $ctrl.widgets);
//clearDataFromCache()
}
old function I wrote:
function clearDataFromCache() {
let widgetObjectsInCache = widgetsCache.get('widgets');
widgetObjectsInCache.forEach((widget) => {
if (widget.chart) delete widget.chart;
if (widget.chartConfig) delete widget.chartConfig;
})
console.log(widgetObjectsInCache, "THE CACHE AFTER REMOVING ANY DATA RELATED STUFF");
}
You can use resolve function for state to refetch the data you want. Please check https://medium.com/opinionated-angularjs/advanced-routing-and-resolves-a2fcbf874a1c?swoff=true#.bkudisx52

How to implement path aliases in ui-router

I'm trying to find a way to implement route aliases in Angular.js using ui-router.
Let's say I have a state with the url article/:articleId and I want to have /articles/some-article-title redirected (internally) to /article/76554 without changing the browser location.
Could this be done with ui-router?
I. Doubled state mapping (reuse of controller, views)
NOTE: This is original answer, showing how to solve the issue with two states. Below is another approach, reacting on the comment by Geert
There is a plunker with working example. Say we have this two objects (on a server)
var articles = [
{ID: 1, Title : 'The cool one', Content : 'The content of the cool one',},
{ID: 2, Title : 'The poor one', Content : 'The content of the poor one',},
];
And we would like to use URL as
// by ID
../article/1
../article/2
// by Title
../article/The-cool-one
../article/The-poor-one
Then we can create this state definitions:
// the detail state with ID
.state('articles.detail', {
url: "/{ID:[0-9]{1,8}}",
templateUrl: 'article.tpl.html',
resolve : {
item : function(ArticleSvc, $stateParams) {
return ArticleSvc.getById($stateParams.ID);
},
},
controller:['$scope','$state','item',
function ( $scope , $state , item){
$scope.article = item;
}],
})
// the title state, expecting the Title to be passed
.state('articles.title', {
url: "/{Title:[0-9a-zA-Z\-]*}",
templateUrl: 'article.tpl.html',
resolve : {
item : function(ArticleSvc, $stateParams) {
return ArticleSvc.getByTitle($stateParams.Title);
},
},
controller:['$scope','$state','item',
function ( $scope , $state , item){
$scope.article = item;
}],
})
As we can see, the trick is that the Controller and the Template (templateUrl) are the same. We just ask the Service ArticleSvc to getById() or getByTitle(). Once resolved, we can work with the returned item...
The plunker with more details is here
II. Aliasing, based on native UI-Router functionality
Note: This extension reacts on Geert appropriate comment
So, there is a UI-Router built-in/native way for route aliasing. It is called
$urlRouterProvider - .when()
I created working plunker here. Firstly, we will need only one state defintion, but without any restrictions on ID.
.state('articles.detail', {
//url: "/{ID:[0-9]{1,8}}",
url: "/{ID}",
We also have to implement some mapper, converting title to id (the alias mapper). That would be new Article service method:
var getIdByTitle = function(title){
// some how get the ID for a Title
...
}
And now the power of $urlRouterProvider.when()
$urlRouterProvider.when(/article\/[a-zA-Z\-]+/,
function($match, $state, ArticleSvc) {
// get the Title
var title = $match.input.split('article/')[1];
// get some promise resolving that title
// converting it into ID
var promiseId = ArticleSvc.getIdByTitle(title);
promiseId.then(function(id){
// once ID is recieved... we can go to the detail
$state.go('articles.detail', { ID: id}, {location: false});
})
// essential part! this will instruct UI-Router,
// that we did it... no need to resolve state anymore
return true;
}
);
That's it. This simple implementation skips error, wrong title... handling. But that is expected to be implemented anyhow... Check it here in action

How to use $resource in Angular to work with a RESTful api

I'm trying to add some basic CRUD functionality to MEAN stack. I've created a RESTful service that works and I'm confused about how to wire it all up. I can get it to work, but I want to make sure I'm doing things the best way and not creating an unnecessary hack.
My api route for a single Person is like this:
// Find one person
app.get('/api/person/:id', function(req, res) {
Person.find ( {_id: req.params.id },
function(err, data){
res.json(data);
}
)});
// Find group of people
app.get('/api/person', function(req, res) {
// use mongoose to get all people in the database
Person.find(function(err, data) {
res.json(data);
});
This seems to work in that if I go to a URI with an ID, as in localhost://3000/api/person/23423434, I see JSON data like this:
[
{
"_id": "532d8a97e443e72ef3cb3e60",
"firstname": "Horace",
"lastname": "Smith",
"age": 33
}
]
This tells me the basic mechanics of my RESTful api are working. Now I'd like to display that data with angular in a template like so:
<h3>{{ person.firstname + ' ' + person.lastname }} </h3>
To do that, I just need to create a $scope.person object with get() or query(). Here's the relevant part of my app:
angular.module('crudApp', ['ngRoute', 'ngResource'])
.config(['$routeProvider', function($routeProvider){
$routeProvider
.when('/api/person/:id',
{
templateUrl: 'partials/person.html',
controller: 'PersonCtrl'
});
}])
.factory('Person', function($resource){
return $resource('api/person/:id', { id: '#_id'});
})
.controller('PersonCtrl', function($scope, $routeParams, Person){
$scope.person = Person.get( { id: $routeParams.id } ); // Having trouble here!
});
The trouble I'm having is that get() fails with an error (Error: [$resource:badcfg]). On the other hand, if I use Person.query(), I get back an array, which means I need to change my template to the following:
<h3>{{ person[0].firstname + ' ' + person[0].lastname }} </h3>
This works, but seems strange and isn't like what I've seen in angular tutorials. The only other solution I've found is to set $scope.person in a callback:
Person.query({ id: $routeParams.id }, function(person){
$scope.person = person[0];
});
This works with my original unmodified template. Is it the best or right way to work with RESTful apis like this? Is there a better way?
Answer: the answer is in comment below. My problem is that api is using Person.find() but should be using Person.findOne( { _id: req.params.id }); Using findOne() returns a single object.
Your api should look like this:
route -> '/api/person/:id'
return single person
route -> '/api/person'
return array of persons
then if you want to get by id, you shall use get method, or if you want to get all persons, you should use query method. Your mistake is that you shall return single object when getting by id

Angular Factory Manipulating Data (CRUD) with function in the Factory

I have a simple Angular App that uses a factory to pull a list of items from a "JSON" file. Eventually this will be connected to a database but for now I'm starting with just pulling from a static file. The JSON file contains an array of items. I'm really trying to understand how do I get a reference to my data in the factory after the promise has been returned to the controller.
My Factory is setup as follows:
angular.module("myApp").factory("ServiceTypeFactory", ['$http', function ($http ) {
return {
ServiceTypes: function () {
return $http.get('json/servicetypes.json').success
(
function (data) {
return data;
});
},
find: function (id) {
return _.findWhere(data, {ID:id}); // Not sure how to get access to the data
}
}
}]);
This works great I can share the Factory across multiple controllers. The part I'm missing is how do I reference a specific array item from my json file and update it in my controller. I'm not following how to get a reference to the actual data so when I modify the item in one controller it the change would be reflected in another controller.
In both of my controllers I have the following code to get a reference to the data initially.
var popService = function (data) {
$scope.ServiceTypes = data;
}
// IF at ANY time the Service Types have not been loaded we will populate them.
if (typeof $scope.ServiceTypes === "undefined") {
ServiceTypeFactory.ServiceTypes().success(popService);
}
My understanding is my $scope.ServiceTypes has really a reference to the data. How do I back in my factory in a function get access to the actual single source of my data. I get that the factory returns the data with functions an object but I'm missing how to reference this data back in my factory to manipulate it. In the future I want to perform CRUD operations on it for the time being I'm just trying to work out the mechanics.
What my JSON file looks like:
{
"serviceTypes": [
{
"ID": "1001",
"ServiceTypeName": "111111",
"Description": "aaaaaaa"
},
{
"ID": "1002",
"ServiceTypeName": "222222",
"Description": "bbbbbbb"
},
{
"ID": "1003",
"ServiceTypeName": "3333333",
"Description": "ccccccc"
},
{
"ID": "1004",
"ServiceTypeName": "444444",
"Description": "dddddddd"
},
{
"ID": "1005",
"ServiceTypeName": "5555555",
"Description": "eeeeeee"
}
]
}
Just needed to clean the code up a little. Watching a couple of quick example on egghead.io made it clear.

Backbone Model not compatible with underscore and ASP.NET MVC Web API Controller?

This is a two stage problem when working with backbone.js and a web api controller.
I have a simple web api controller that returns a JSON string, in fiddler the result looks like this:
{
"$type": "MvcApplication.Models.Article, MvcApplication",
"Id": "1",
"Heading":"The heading"
}
I use the following code to fetch a user from my web api
var user = new Usermodel({ id: "1" });
user.fetch({
success: function (u) {
console.log(u.toJSON());
}
});
now my backbone user object looks like this
{
id: "1",
{
"$type": "MvcApplication.Models.Article, MvcApplication",
"Id": "1",
"Heading": "The heading"
}
}
When I try to bind this backbone model object to my view template that looks like this
<form>
<input type="text" value="<%=Heading%>" />
<input type="submit" value="Save" />
</form>
i get, Heading is undefined but when I use id it binds just fine? It seems like underscore does not like the backbone model object and just want a plain JSON object just like the one I get from my web api?
The second problem with this is that when I save my model with user.save({ Heading: "my new heading }); the payload to my web api is the backbone model which is completely wrong because my api expects a user object like this to be sent to the server:
{
"$type": "MvcApplication.Models.Article, MvcApplication",
"Id": "1",
"Heading":"The heading"
}
and not the backbone model with the real object wrapped inside. Is it possible to solve so that underscore can handle backbone models and tell backbone to only send the payload that my end point expects?
You may be able to solve the problem by following these steps:
In addition to using fiddler to inspect your response, look at the response on the network tab of Chrome Developer Tools. If the response does not look like this, then your web api is not returning a valid json response, the problem is most likely within your web api. You need to get/provide more information about your web api to solve the problem. Verify that the response looks like this:
After verifying that the response from the web api is correct, check out the following jsfiddle I modified:
http://jsfiddle.net/J83aU/23/
Fix your client side code referencing the example I have provided.
Properly instantiate the Backbone objects.
Call the view.render function at the correct step, after the response is received from the server.
Make sure that the main content div is actually rendered before creating a view which depends on it for the 'view.el' property.
Declare the 'view.el' property properly, with a string rather than jQuery object.
Use development Backbone and underscore to enable debugging, an important concept when learning to use open source frameworks such as Backbone.
Use jsfiddle's echo/json api to mock a valid ajax json response, exactly as described in step 1.
The following json example you submitted is not even valid json, if you update your question with valid json example, it would be easier to solve the problem. It is unlikely that Backbone created this non-json structure and more likely that you have submitted it here incorrectly.
{
id: "1",
{
"$type": "MvcApplication.Models.Article, MvcApplication",
"Id": "1",
"Heading": "The heading"
}
}
Finally, try to provide a screenshot of the http headers or something for the problem that is occurring when you call model.save().
Read over the Backbone documentation for model.save() and make sure you are doing everything just as the example provided.
You may be able to workaround Backbone's funky save function by forcing your attributes into POST parameters using ajax options:
$.fn.serializeObject = function(){
var o = {};
var a = this.serializeArray();
$.each(a, function() {
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};
var saveView = Backbone.View.extend({
events:{
'click #saveSubmitButton':'submit'
},
submit:function (event) {
event.preventDefault();
var view = this,
attributes = $('#saveForm').serializeObject();
this.model.save(attributes, {
data:attributes,
processData:true,
success:function (model) {
//....
}
});
},
render:function () {
//.......
}
});
The attributes property of your model should be unaltered. Send those to your template call:
var MyModel = Backbone.Model.extend();
var newModel = new MyModel({
"$type": "MvcApplication.Models.Article, MvcApplication",
"Heading":"The heading"
});
var html = _.template(templateVar, newModel.attributes);
In your templateVar, which is your templated markup, you should be able to reference $type and Heading directly.
If you have a look at the jsFiddle through a debugger like Firebug you can see that the way you construct the model's URL is not working out, because the forward slash gets encoded. Can you try to modify your model declaration to this:
var Usermodel = Backbone.Model.extend({
url: function () {
return '/api/page/articles/' + this.get('id');
}
});
var user = new Usermodel({
id: '85'
});
And see if you still get the same JSON. Basically if you don't have a Backbone.sync override you are using built-in retrieval that for one shouldn't produce invalid JSON.

Resources