I have a DB with a collection of games and a collection of comments about those games. each game has a unique ID and each comment has a field game_id.
in the client side, I wrote an async function in the scope that retrieves the number of comments for a game from the db and return a promise. what is the right (and most cleanest) way to show the value in the HTML ?
I know there are different ways to do this. I am looking for the simplest and cleanest way.
Will this code work?
//code in HTML
<div ng-repeat="game in games">
<span>{{game.date}}</span>
<span>{{game.home}}</span>
<span>{{game.away}}</span>
<span>{{getNumberOfComments(game.id)}} Comments</span>
</div>
app.factory('apiService', function($http) {
return {
getGames: function() {
return $http.get('/api/games',{cache: true});
},
getCommentsCount: function(game_id) {
return $http.get('/api/CommentsCount/' + id);
}
};
});
app.controller('MyCtrl', function ($scope, apiService) {
gameService.getGames().then(function(resp) {
$scope.games = resp.data;
});
$scope.getNumberOfComments = function(id){
return apiService.getCommentsCount(id).then(function(resp){
return resp.data;
});
});
});
Followup question:
if I have many games (lets say 30), is it a bad idea to send 30 HTTP requests to the server for each game? is it better to send one request called "getAllComments" and retrieve the comments count for each game from the repsonse ?
for example:
function getAllComments() {
$http.get('/allComments',function(data){
$scope.games.forEach(function(game){
game.commentsCount = data[game.id];
});
});
}
//code in HTML
<div ng-repeat="game in games">
<span>{{game.commentsCount}} Comments</span>
</div>
This seems like a good solution - but the problem starts when i have more than 30 games but i want to show 30 games to the user and give him the possibility to load more. Then im actually asking more comments than I need and i have to run this function every time the user loads more games. it seems like a complicated and efficient solution.
EDIT:
One solution is to aggregate the information on the server side and have the client send 1 HTTP request and the response will include all the information. To implement this I will need to iterate the list of games in the server side and for each game retrieve the comments count from the comment collection. The downside of this solution is that the it will take longer for the client to see the list of games. I rather the client to first see the list of games as soon as possible and then after that to load the number of comments. Another downside is the in NodeJS (the backend framework that I'm using), a code like that will block the main thread from serving other clients. Therefore I do not want to make any iterations on the server side.
One more thing that needs to take into account is caching. I want to save the HTTP request that retrieves the games in the $http cache. Because the list of games is static and not going to change, unlike the comments count, that is always increasing. This way when the client moves to a different route and comes back to the games list, the games will show up immediately. But I do want to load the comments count every time the user comes back to the games page.
A clean way to do this would be to first move the code that makes the http call to a service:
app.factory('gameService', function($http) {
return {
getGames: function() {
return $http.get('/api/games');
}
};
});
Then, inject the service in your controller, call the get method and set the value on scope to be used by the view:
app.controller('MyCtrl', function ($scope, gameService) {
$scope.model = {};
gameService.getGames().then(function(resp) {
$scope.model.games = resp.data;
});
});
And your html would be like this:
<div ng-repeat="game in model.games">
<span>{{game.comments.length}} Comments</span>
</div>
Note that this assumes that the endpoint returns all the games and each game has a field for the comments. For example, the schema for a Game might look like this:
{
"id": "1",
"name": "The best game",
"comments": [
{
"id": "c1",
"content": "The is the best game ever"
}
]
}
Related
I have a simple angular resource that I've defined as below:
CompanyService.factory('CompanyService',
function ($resource) {
return $resource('https://baseurl.com/api/values/');
}
);
I then have a controller that calls that resource passing in a success and fail function:
.controller('companyList', function($scope, CompanyService) {
$scope.companies = CompanyService.query(
function(data) {
console.log(data);
return data;
},
function(error){
console.log("Error:");
console.log(error);
}
);
The rest API is a .NET MVC Web API that is extremely basic. I've configured it to return JSON and it simply returns an array of two objects like below. I've also enabled CORS so my angular app, which is hosted in a different domain, can call the api.
[{ID:1, Name:"TEST1"}, {ID:2, Name:"TEST2"}]
I've tested the REST call using jquery and just straight call through browser. All was functional (including the cross site scripting when calling from my angular app just using a straight JavaScript HTTP call).
When I try to call the api from my controller however, it always ends up in the error function. The error object contains a data property that is always populated with the string "resource is required|resource is required|undefined"
When I check the network I see no call to the values end point. It's as if the call is failing before ever being made.
If I change out the url to point to some sample REST api like https://jsonplaceholder.typicode.com/users/ it works fine and I'm able to see the call to "users" in the network traffic, which makes me think there is something wrong with my C# REST endpoint, however all my tests to call the REST endpoint outside of angular work successfully.
Can anyone help? I can't find anyone reporting this issues before anywhere on the net.
should the code be the one below? i didn't test it, just guess.
myModule.factory('CompanyService',
function ($resource) {
return $resource('https://baseurl.com/api/values/');
}
)
.controller('companyList', function($scope, CompanyService) {
CompanyService.query(
function(data) {
$scope.companies = data;
console.log(data);
return data;
},
function(error){
console.log("Error:");
console.log(error);
}
);
I ended up rebuilding my angular app from scratch. My first app was from the angular-seed github and had a handful of libraries already added in for testing and other things. One of those things is was was leading to this error as once I started a new project completely from scratch and added in angular and my REST call things worked perfectly. I've already spent too much time working through this so not going to spend any more time identifying exactly what it is but in case anyone else runs into the problem I did want to answer this one and close the book on it.
This question already has answers here:
AngularJS : Initialize service with asynchronous data
(10 answers)
Closed 7 years ago.
I have a link generator service that is able to generate links to specific content types (users' details page, content items' details pages etc).
This service is really easy to use and has synchronous functions:
links.content(contentInstance); // /items/123
links.user(userInstance); // /users/234
I now have to introduce separate routing for logged in user to change from /users/id to /users/me.
The only change I'd need to add to my link generator service is to check whether userInstance.id == loggedInUser.id and return a different route URL. This is not a problem as long as my logged-in user's info would be synchronously available. but it's not...
I have a userService.getMyInfo() that returns a promise. The first time it's called it actually makes a server request but subsequent calls return a resolved promise with already cached data.
So how should I implement my user link URL generation in my link generator service?
Edit
Ok. So to see better what I have at the moment and where I'm having the problem. I'm pretty aware that async will stay async and that it can't be converted to synchronous (and it shouldn't be).
This is some more of my code, that will make it easier to understand.
linkGenerator.user
angular
.module("App.Globals")
.factory("linkGenerator", function(userService) {
...
user: function(userInstance) {
// I should be calling userService.getMyInfo() here
return "/users/{0}/{1}".format(userInstance.id, userInstance.name);
},
...
});
userService.getMyInfo
angular
.module("App.Globals")
.service("userService", function($q, cacheService, userResource) {
...
getMyInfo: function() {
if (cacheService.exists("USER_KEY"))
// return resolved promise
return $q.when(cacheService.get("USER_KEY"));
// get data
return userResource
.getCurrentUser()
.$promise
.then(function(userData) {
// cache it
cacheService.set("USER_KEY", userData);
});
},
...
});
Controller
angular
.module("App.Content")
.controller("ItemDetailsController", function(linkGenerator, ...) {
...
this.model = { ... };
this.helpers = {
...
links: linkGenerator,
...
};
...
});
View
View uses ItemDetailsController as context notation.
...
<a ng-href="{{::context.helpers.links(item.author)}}"
ng-bind="::item.author.name">
</a>
...
Notes
As you can see my view generates links to item authors. The problem is that my linkGenerator (as you can see from the code may not have the information yet whether it should generate one of the correct links to user details view.
I know I can't (and don't want to) change my async code to synchronous, but what would be the best way to make this thing work as expected?
One possible solution
For the time being I've come up with a solution that does the trick, but I don't really like it, as I have to supply my logged in user's ID to linkGenerator.user(userInstance, loggedInUserId) function. Then I set up my routing so that I add resolve to my route where I call userService.getMyInfo() which means that my controller is not being instantiated until all promises are resolved. Something along this line:
routeProvider
.when("...", {
templateUrl: "path/to/my/details/template",
controller: "ItemDetailsController".
controllerAs: "context",
resolve: {
myInfo: function(userService) {
return userService.getMyInfo();
}
}
})
...
Then I also add an additional helper to my controller
this.helpers = {
...
links: linkGenerator,
me: myInfo.id,
...
};
And then I also change link generator's function by adding the additional parameter that I then supply in the view.
linkGenerator.user = function(userInstance, loggedInUserId) {
if (userInstance.id === loggedInUserId)
return "users/me";
return "users/{0}/{1}".format(userInstance.id, userInstance.name);
}
and in the view
<a ng-href="{{::context.helpers.links.user(item.author, context.helpers.me)}}"...
And I don't to always supply logged in user's ID. I want my service to take care of this data on its own.
There is no way to make anything in JavaScript that is asynchronous at some point synchronous again. This is a ground rule of how concurrency works - no blocking for waiting for stuff is allowed.
Instead, you can make your new method return a promise and use the regular tools for waiting for it to resolve.
links.me = function(){
var info = userService.getMyInfo();
return info.then(info => { // or function(info){ if old browser
// generate link here
return `/users/${info.id}`; // or regular string concat if old browser
});
}
Which you'd have to use asynchronously as:
links.me().then(function(link){
// use link here
});
I am using Restangular on my front-end and I want to query this URL:
http://localhost:4000/study?projectId=123456
It returns a list of studies associated to a certain project. So I have the following controller code for the moment:
MyApp.controller('ProjectDetailCtrl', function ($scope, $routeParams, Restangular) {
// [...]
var resource = Restangular.all('study');
console.log($routeParams.projectId); // displays "123456", the current projectId
resource.getList('study', {projectId: $routeParams.projectId}).then(function(studies){
// Doing stuff
});
});
But it does not return the data I want. Indeed, when I look in the Network panel of Firefox, it queries http://localhost:4000/study, as if the parameter was not specified...
It seems to be the same code that the author give on Github, and every question on the web seems to use the same syntax. Why is my parameter ignored?
The first parameter of the getList should be your query params. You've already specified the study bit of your url. You had an extra "study".
var resource = Restangular.all('study');
resource.getList({ projectId: $routeParams.projectId }).then(function(studies) {
// do stuff
});
WARNING: fairly long question
I am new to angular and I've gone through a few tutorials and examples such as the official tutorial on the angularjs website. The recommended way to get data into your app and share that data between controllers seems very clear. You create a service that is shared by all controllers which makes an asynchronous request to the server for the data in JSON format.
This is great in theory, and seems fine in the extremely simple examples that only show one controller, or when controllers don't share logic that depends on the shared data. Take the following example of a simple budget application based on yearly income and taxes:
Create the app with a dependency on ngResource:
var app = angular.module('budgetApp', ['ngResource']);
NetIncomeCtrl controller that handles income and tax items, and calculates net income:
app.controller('NetIncomeCtrl', function ($scope, BudgetData) {
var categoryTotal = function (category) {
var total = 0;
angular.forEach(category.transactions, function (transaction) {
total += transaction.amount;
});
return total;
};
$scope.model = BudgetData.get(function (model) {
$scope.totalIncome = categoryTotal(model.income);
$scope.totalTaxes = categoryTotal(model.taxes);
});
$scope.netIncome = function () {
return $scope.totalIncome - $scope.totalTaxes;
};
});
BudgetData service that uses $resource to retrieve the JSON data from the server:
app.factory('BudgetData', function ($resource) {
return $resource('data/budget.json');
});
budget.json contains the JSON data returned from the server:
{
"income": {
"transactions": [
{
"description": "Employment",
"amount": 45000
},
{
"description": "Investments",
"amount": 5000
}
]
},
"taxes": {
"transactions": [
{
"description": "State",
"amount": 5000
},
{
"description": "Federal",
"amount": 10000
}
]
},
}
Then on my screen I have two repeaters that show the income and tax items (which you can edit), and then the net income is calculated and displayed.
This works great, and is the standard approach I've seen used in tutorials. However, if I just add one more controller that depends on some of the same data and logic, it begins to unravel:
ExpensesCtrl controller for expenses which will in the end calculate the surplus (net income - expenses):
app.controller('ExpensesCtrl', function ($scope, BudgetData) {
var categoryTotal = function (category) {
var total = 0;
angular.forEach(category.transactions, function (transaction) {
total += transaction.amount;
});
return total;
};
$scope.model = BudgetData.get(function (model) {
$scope.totalIncome = categoryTotal(model.income);
$scope.totalTaxes = categoryTotal(model.taxes);
$scope.totalExpenses = categoryTotal(model.expenses);
});
$scope.netIncome = function () {
return $scope.totalIncome - $scope.totalTaxes;
};
$scope.surplus = function () {
return $scope.netIncome() - $scope.totalExpenses;
};
});
budget.json adds the expenses data:
"expenses": {
"transactions": [
{
"description": "Mortgage",
"amount": 12000
},
{
"description": "Car Payments",
"amount": 3600
}
]
}
Then on a separate part of the screen I have a section that uses this controller and uses a repeater to show the expense items, and then re-displays the net income, and finally shows the resulting surplus.
This example works, but there are several problems (questions) with it:
1) In this example I've managed to keep most of my controller's logic out of the callback function, but all controllers start out in a callback because everything depends on the model being loaded. I understand this is the nature of javascript, but angular is supposed to reduce the need for all of these callbacks, and this just doesn't seem clean. The only reason I was able to take some of the logic out of the callbacks here is because of the magic that angular does under the hood to substitute a fake object for the model until the model gets loaded. Since this 'magic' is not intuitive, it's hard to tell if your code will work as expected.
Is there a consistent way people deal with this? I really don't want some elaborate solution that makes this 101 intro app into something really complicated. Is there a simple and standard approach to restructuring this code somehow to avoid so many callbacks and make the code more intuitive?
2) I have a bunch of repeated logic. The categoryTotal and netIncome logic should only be in one place, and shared between controllers. These controllers are used in completely separate parts of the screen, so they can't use scope inheritance. Even when using scope inheritance there could be problems, because the child controller's scope can't depend on the parent scope's model being loaded even when its own model is loaded.
The categoryTotal logic is just a helper function that isn't tied directly to the model, so I don't know where you put generic helper functions in angular. As for the netIncome which does directly depend on the model and needs to be in the scope, it would be possible to add it to the service or the model. However, the service should only be concerned with retrieving the model from the server, and the model should just contain data, and be as anemic as possible. Also, it seems like this kind of logic belongs in a controller. Is there a standard way to deal with this?
3) Every time the service is called to fetch the data, it does an http request to the server, even though the data is the same every time. The model should be cached after the first request, so there is only one request made.
I realize I could handle this in the service. I could just store the model as a local variable and then check that variable to see if the data already exists before making the request. It just seems odd that none of the tutorials I read even mentioned this. Also, I'm looking for a standard 'angular' way of dealing with it.
Sorry for the incredibly long post. I would really like to keep this app at the 101 intro level, and not get into really complicated areas if possible. I'm also hoping there is a standard 'angular' way to deal with these problems that I have just not come across yet.
Thanks in advance!
This is how I do it. You create a service that handles data, and if it is changed it broadcasts messages to your controllers. It will get the initial data that can be gotten with BudgetData.data. If anyone changes the data
.service("BudgetData", function($http, $rootScope) {
var this_ = this, data;
$http.get('wikiArticles/categories', function(response) {
this_.set(response.data);
}
this.get = function() {
return data;
}
this.set = function(data_) {
data = data_;
$rootScope.$broadcast('event:data-change');
}
});
In your controller you just need to listen for the events, and it will update your scope variables accordingly. You can use this in as many controllers as you want.
$rootScope.$on('event:data-change', function() {
$scope.data = BudgetData.get();
}
$scope.update = function(d) {
BudgetData.set(d);
}
I found it is very hard to deal with flash message returned from backend when working with single-page-application.
Say there are five actions:
login, logout,search,add,delete.
each action emits messages(successful or failed,could be more). in a Non-SPA, it is easy enough to do something like backend.getFlash() in the template to get the message stored in session.
However, in SPA, if the results are returned in JSON, and of course backend can pass the message to a js framework, say angularjs. But it is really tedious and not flexible at all.
Does anyone have any idea or experience?
Thanks
From your question I understood that what you really want is to have some messages for every actions, say for delete you need to display a message like "Deleted Successfully" and you don't want this message to be send along with the result set from your backend.
In AngularJS you can achieve this by having a service for showing the messages.
Here is an example;
Create a service like this in AngularJS
app.factory('FlashService', function ($rootScope) {
return {
show: function (message) {
$rootScope.flash = message;
return message;
},
clear: function () {
$rootScope.flash = '';
return null;
}
};
});
Here FlashService is the name of the service you are creating, don't bother about the name you can give any name of your choice.
To use this service inject it anywhere you want like;
var app = angular.module("your app name", []);
app.controller("MyCtrl", function(FlashService) {
FlashService.show("I am here");
});
And use it in your html like
<div id="flash">{{flash}}</div>