The angularjs guide contains an example detailing an async call in a service.
The following code is given
angular.module('finance3', [])
.factory('currencyConverter', ['$http', function($http) {
var currencies = ['USD', 'EUR', 'CNY'];
var usdToForeignRates = {};
var convert = function(amount, inCurr, outCurr) {
return amount * usdToForeignRates[outCurr] / usdToForeignRates[inCurr];
};
var refresh = function() {
var url = 'https://api.exchangeratesapi.io/latest?base=USD&symbols=' + currencies.join(",");
return $http.get(url).then(function(response) {
usdToForeignRates = response.data.rates;
usdToForeignRates['USD'] = 1;
});
};
refresh();
return {
currencies: currencies,
convert: convert
};
}]);
Since the refresh function is async, how does angularjs ensure that the data is loaded before loading the controller which accesses the data returned by the refresh function, ie the data contained in the usdToForeignRates object.
I assume there is some blocking going on somewhere, otherwise when the controller accesses the object returned, it will get undefined values.
I want to understand the flow, does angular internally ensure that service loads before injecting it into the controller?
Related
I've got a sequence of json files (page1.json, page2.json, etc) that contain the layout details for our pages.
Up until now I've been loading the data for each page when it's needed and everything has been working well, but it's now clear that the page data for every page is needed when the angular app starts - and each page must be in a separate file (to please the designers).
Here's what I've been trying:
var app = angular.module("templatesApp", ['bulletComponent', 'popupComponent', 'imageComponent', 'paragraphComponent', 'ngRoute']);
var numberOfPages = 0;
var pageData = [];
var pageIndex = 0;
/*Factory that runs http.get on content.json, used by multiple controllers */
app.factory('content', ['$http', 'completions', 'pages', function($http, completions, pages) {
var url = 'json/content.json';
return $http.get(url)
.success(function(data) {
// loop through pages and add corresponding page objects to completions.js service
for (var i=0; i<data.length; i++) {
completions.addPage(data[i].title);
numberOfPages += 1;
}
loadPageData(pages, completions);
return data;
})
.error(function(data) {
return data;
});
}]);
var loadPageData = function(pages, completions) {
console.log("loadPageData called when pageIndex = "+pageIndex);
pages.success(function(loadedData) {
pageData.push(loadedData);
completions.addPageComponents(pageIndex + 1, pageData[pageData.length-1]);
pageIndex += 1;
if (pageIndex < numberOfPages) {
loadPageData(pages, completions);
}
});
}
app.factory('pages', ['$http', function($http) {
console.log("pages factory called with pageIndex = "+pageIndex);
var url = 'json/page'+ (pageIndex + 1) +'.json';
return $http.get(url)
.success(function(loadedData) {
return loadedData;
})
.error(function(loadedData) {
return loadedData;
});
}]);
/*Controller for menu, data fetched from factory*/
app.controller('menuCtrl', ['$scope', '$http', 'content', function($scope, $http, content) {
content.success(function(data) {
$scope.pages = data;
});
}]);
/*Controller for individual pages. Makes XHR to json page for pages data.
Has function for calculating column widths*/
app.controller('pageCtrl', ['$scope', '$routeParams', '$http', 'content', 'completions', function($scope, $routeParams, $http, content, completions) {
$scope.page = pageData[$routeParams.pageId-1];
$scope.getStyle = function(singleCase, device) {
if (singleCase == undefined)
return '';
return assignBootstrap(singleCase, device);
}
$scope.pageId = $routeParams.pageId;
}]);
So hopefully you can see, the 'content' factory gets called by 'menuCtrl'. It first loads the content.json file (a list of the page titles) then it calls 'loadPageData', a recursive function that should call the 'pages' factory on every required page, as it loads.
The problem is, those console.logs show that the 'pages' factory is getting called just once and BEFORE even the loadPageData function. Each html page is then being passed the same (first) set of data.
I presume I'm not using the 'pages' factory correctly, but I don't know what the correct options are.
If anyone has any pointers, I'd be keen to hear them.
EDIT
Thanks for the comments. I think I've realised what I need to know is: How do I set up a chain of promises ($http calls) dynamically? The number of pages will vary, so I can't hard-code them.
Here's what I ended up doing. Not sure if this is 100% best practise, but it seems to be working well:
app.factory('content', ['$http', 'completions', function($http, completions) {
var url = 'json/content.js';
return $http.get(url)
.success(function(data) {
// loop through pages and add corresponding page objects to completions.js service
for (var i=0; i<data.length; i++) {
completions.addPage(data[i].title);
numberOfPages += 1;
}
// load page .js files
var tempData = getData(0, completions);
for (var i = 1; i < numberOfPages; i++) {
(function (i) {
tempData = tempData.then(function() {
return getData(i, completions);
});
}(i));
}
return data;
})
.error(function(data) {
return data;
});
}]);
// loads individual page data, stores in pageData and sets up SCORM objectives from completable components (in completions service)
function getData(id, completions) {
return $.ajax({
url: 'json/page'+ (id + 1) +'.js',
dataType: 'json'
}).done(function(d) {
// push data into pageData array, for use later when pages are routed to
pageData.push(d);
// register this page's completable components in SCORM
completions.addPageComponents(id + 1, d);
}).fail(function() {
console.log('ERROR loading page index '+id);
});
}
I couldn't make sense of $q.all or promises, but a colleague pointed me to another SO answer that used something similar to the above.
When the 'content' factory is called, it calls the getData function to load the first .js file. Looping through up to the number of pages, I'm guessing the line...
tempData = tempData.then(...
...means that it waits for that load to complete before starting the next. The key thing for storing the currently loaded data is what happens in the getData.done function; The loaded data is pushed into pageData array for use later and the data is also passed into the completions service (to set up our objectives in SCORM).
All working as intended!
I am using three Angular controllers:
**Controller1**
var fetchStudentDetails = function(){
var sDetails = myService.getList(//url-1 here);
sDetails.then(function (data) {
$scope.studentData = data.list;
var studentId = $scope.studentData[0].id;
});
}
fetchStudentDetails();
$scope.loadSecondLevel = function(){
$state.go('secondLevel');
}
**Controller2**
var fetchClassDetails = function(){
var sDetails = myService.getList(//url-2 here);
sDetails.then(function (data) {
$scope.classData = data.list;
var className = $scope.classData[0].name;
});
}
fetchClassDetails();
$scope.loadThirdLevel = function(){
$state.go('thirdLevel');
}
**Controller3**
$scope.putStudentDetails = function(){
// Here I need studentId,className for updateResource
var sDetails = myService.updateResource(//url-3 here);
sDetails.then(function (data) {
});
}
Where I have to pass studentId (in Controller1), className (in Controller2) into a function which in Controller3. I tried with $rootScope, it is working but when refresh the page $rootScope values become empty. Does anyone know how to do this?
Your question could be split into two aspects:
1. How to share data between controllers
The best practice to share data in Angular 1.x is using factory, store the shared data in a factory service, and expose access methods to controllers:
factory('DetailData', function(myService, $q){
var _details;
function __getDetailData(){
return details
}
function __setDetailData(){
return myService.getList().then(function(data){
_details = data;
})
}
return {
getDetailData: __getDetailData,
setDetailData: __setDetailData
}
})
controller('myContrller', function(DetailData, $scope){
$scope.data = DetailData.getDetailData();
})
2. How to persist data when page refreshed,
you can use localStorage to keep data persistent during page reloading, many tools & libraries can achieve this, for example ngStorage, or you could reset the data from server every time your angular application started:
//this would register work which would be performed
//when app finish loading and ready to start.
angular.module('app').run(function(DetailData){
DetailData.setDetailData();
})
Depending on what problem you are solving.
There are three options:
Is to save data to $rootScope
Is to use $scope.$emit & $scope.$on functions.
Use a custom Service to store the data
And if you need to save data, so it was available after full page reload - localStorage.
Hey this question are responded in Passing data between controllers in Angular JS?
But the simple response is in the services.
I have a object in mainController.js that is set as default as 99.
I am obtaining user location and do running some other function with it to calculate this value.
However, When I load the page, the page seems to load faster than this process. Therefore it displays 99 instead of the calculated value.
If I put console.log after the calculation, the object is successfully changed.
edit1:
status.success( function(data)
{
$scope.current = data;
$scope.$broadcast('back_end_connected');
});
$scope.getLocation = function()
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
var dirBuilt = false;
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
}
};
});
that "alert("aaa: "+ $scope.facilities[0].distance);" returns the value I want it to display but it is not displayed on the page....
(ng-bind would not work for some reason...)
How can I make the html wait for the operation? Thanks
You should not make HTML wait until you finish your Js code ! What you should be doing is showing a placeholder value - Loading image so that user know the page is loading some data.
Once you are done with your calculation, hide /replace the loading image with the data you want to show.
Quick example
Your view markup will have some HTML element to show the progress bar.And all your other contents will be in another div
<body ng-app="yourApp" ng-controller="yourCtrl as vm">
<div ng-show="loaderCount>0"> Loading something..</div>
<div ng-show="loaderCount==0">
<h4>{{userScore}}</h4>
</div>
</body>
And in your angular controller, You have a scope variable called loaderCount which you will increase everytime when you are doing some operation (http call/Long running function execution etc..). When you get your result, You decrease this variable value back. In your View You are hiding and showing the Loading Pane based on this value.
var yourApp= angular.module('yourApp', []);
var ctrl = function($scope, $http) {
var vm = this;
vm.loaderCount = 0;
vm.someValue = "";
vm.actionItems = [{ Name: "Test" }, { Name: "Test2" }];
vm.loaderCount++;
$http.get("../Home/GetSlowData").then(function(s) {
vm.loaderCount--;
vm.someValue = s.data;
});
};
yourApp.controller('yourCtrl', ['$scope', '$http', ctrl]);
This might not be the best angular code. But this will give you an idea about how to handle this use case. You should be using services to talk to Http endpoints instead of directly using $http in your angular controller.
Note that, $http.get returns a promise which allows you do things when you get the response from this asynchronous operation (the then event). You should make sure that your time taking calculation is returning a promise.
You can bind with the ngBind directive instead of {{}} and not write to the binded property when the calculation is done.
This will hide the result from the view while it is null.
Try to do something like this:
$scope.value = "Loading";
or
$scope.value = "";
$scope.calculate = function() {
$scope.value = yourcalculation;
}
$scope.calculate();
Or in your case i think if you use $scope.$apply() after you add data to your scope element will do the trick
Try this :
$scope.$on('location_obtained', function(){
$scope.buildDist = function()
{
if(dirBuilt === false)
{
$scope.facilities[0].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[0].location.lat,$scope.facilities[0].location.lng);
$scope.facilities[1].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[1].location.lat,$scope.facilities[1].location.lng);
$scope.facilities[2].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[2].location.lat,$scope.facilities[2].location.lng);
$scope.facilities[3].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[3].location.lat,$scope.facilities[3].location.lng);
$scope.facilities[4].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[4].location.lat,$scope.facilities[4].location.lng);
$scope.facilities[5].distance = distCalc($scope.location.lat,$scope.location.lng,$scope.facilities[5].location.lat,$scope.facilities[5].location.lng);
$scope.$broadcast('dist_obtained');
dirBuilt = true;
alert("aaa: "+ $scope.facilities[0].distance);
$scope.$apply(); // This should do the trick
}
};
});
Are you using a promise on the service? The function running your calculation can be in a separate angular factory which has .success and .error events to tap into. If the calculation is a success. You can pass the data back to your controller and then bind that data to the controller scope. I'll be at a computer soon and will add some code to explain further if needed.
This would be your distance calculator, I used a Factory over a service but you can read about why to use one over the other
A couple of things to keep in mind. You geolcation is more of a service then something to control since you're requesting from the user, you could add it to the below factory to expand the factories capabilities and allow you to use getLocation in other controllers.
make sure you add the distance service to your html document
also make sure you put the distance service in your controller and on the main angular app.
distanceService.js
(function(){
'use strict'
var distSrvc= angular.module("distanceService",[])
distSrvc.factory('DistanceCalc',['$http',function($http){
return{
getDistance:function(facilitiesData,locationData){
Object.keys(facilitiesData).length; // or if it's already an array of facilities use facilitiesData.length
// if its a javascript object and distance is already defined
for (var distance in facilitiesData) {
if (facilitiesData.hasOwnProperty(distance)) {
facilitiesData.distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
}
// if its an array of objects
for (var i = 0 ; i< facilitiesData.length;i++){
facilitiesData[i].distance = distCalc(locationData.lat,locationData.lng,facilitieData.location.lat,facilitieData.location.lng);
}
return facilitiesData
}
})();
Then in your controller you'll need to load the service for use.
yourcontroller.js this will give errors if you don't load it on the html page and add it to the main angular app.
(function(){
'use strict'
var myController= angular.module("distanceService",[])
myController.controller('myController',['$scope','DistanceCalc',function($http,DistanceCalc){ // see how i added the service and passed it to the function
$scope.getLocation = function(){
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(function (position){
$scope.location = {lat: position.coords.latitude, lng: position.coords.longitude};
$scope.$broadcast('location_obtained');
$scope.buildDist();
$scope.fetch();
//$scope.getRec();
});
}
else{
alert("Geolocation is not supported by this browser.");
}
};
// this line below sends the data to the service
DistanceCalc.getDistance($scope.facilities,$scope.location)
.success(function(data){
//success i'm done running distance calculations and now i want to update the facilties object with the added distance
$scope.facilities = data
})
.error(function(data){
// some error occurred and data can tell you about that error
})
}
})();
I want build some simple cache in Angularjs service for data provide from http request. Additional I want always get reference to the same object. I prepare example code to illustrate my thinking and problem which I have now.
jsfiddle code illustrate problem
I have service UsersModel which provide me user from http request.This user data are shared between controllers. So want to have always reference to same data. I add to him simple logic. Before UsersModel.getUsers() call service check if exist any data from previous call, if exist return him, if not do a http request. I inject that service in tree controller. In first two controllers UsersModel.getUsers() is call immediately after page load. In last after click on button.
Problem is when two first controller call UsersModel.getUsers() in the same time. Then any cached data don't exist and both do http request After that I have in first two controller reference to different user objects. We can see this clicking on load button.
And now my question. How to make this work for the simultaneous first call UsersModel.getUsers() and always have reference to the same object data.
app.js
var APP = angular.module('APP', []);
APP.SidebarCtrl = function ($scope, UsersModel) {
var sidebarCtrl = this;
UsersModel.getUsers()
.then(function (users) {
sidebarCtrl.users = users;
});
};
APP.ContentCtrl = function ($scope, UsersModel) {
var contentCtrl = this;
UsersModel.getUsers()
.then(function (users) {
contentCtrl.users = users;
});
};
APP.FootCtrl = function ($scope, UsersModel) {
var footCtrl = this;
function load() {
UsersModel.getUsers()
.then(function (users) {
footCtrl.users = users;
});
}
footCtrl.load = load
};
APP.service('UsersModel', function ($http, $q) {
var model = this,
URLS = {
FETCH: 'http://api.randomuser.me/'
},
users;
function extract(result) {
return result.data.results['0'].user.email;
}
function cacheUsers(result) {
users = extract(result);
return users;
}
model.getUsers = function () {
return (users) ? $q.when(users) : $http.get(URLS.FETCH).then(cacheUsers);
};
});
Index.html
<div ng-app="APP">
<div ng-controller="APP.SidebarCtrl as sidebarCtrl">
<h1>{{ sidebarCtrl.users }}</h1>
</div>
<div ng-controller="APP.ContentCtrl as contentCtrl">
<h1>{{ contentCtrl.users }}</h1>
</div>
<div ng-controller="APP.FootCtrl as footCtrl">
<h1>{{ footCtrl.users }}</h1>
<button ng-click="footCtrl.load()" type="button">Load</button>
</div>
</div>
jsfiddle code illustrate problem
You can modify your functions as follows:
function cacheUsers(result) {
return (users) ? users : users = extract(result);
}
and
model.getUsers = function () {
return (users) ? $q.when(users) : $http.get(URLS.FETCH, {cache: true}).then(cacheUsers);
};
It provides additional cache check after fetch and enables built-in cache for the object.
I suggest you to read http://www.webdeveasy.com/angularjs-data-model/
How do I save URL parameters state throughout lifecycle of application using pushState?
Page load.
Go to "/search" via href
submitSearch() through filter fields where $location.search(fields)
Go to "/anotherPage" via href
Go back to "/search" via href
Search paramters are set back to what they last were.
Is this a built in feature somewhere?
If not what's the best way to go about this?
If you're planning on a mostly single page website through pushState, you might want to get an intimate understanding of $routeProvider (http://docs.angularjs.org/api/ngRoute.%24routeProvider).
To go further down the rabbit hole, I would recommend looking at the ui-router module: (https://github.com/angular-ui/ui-router). $stateProvider (from ui-router) and $routeProvider work very similar, so sometimes the ui-router docs can give insights that you can't find in the poor documentation of the $routeProvider.
I reccomend going through the five page ui-router documentation (https://github.com/angular-ui/ui-router/wiki) page by page.
After all that preamble, here's the practical: you would set up a factory that holds history data and use the controller defined in your $routeProvider/$stateProvider to access and manipulate that data.
Note: the factory is a service. A service is not always a factory. The namespace goes:
angular.module.<servicetype[factory|provider|service]>.
This post explains the service types: https://stackoverflow.com/a/15666049/2297328. It's important to remember that they're all singletons.
Ex:
var myApp = angular.module("myApp",[]);
myApp.factory("Name", function(){
return factoryObject
});
The code would look something like:
// Warning: pseudo-code
// Defining states
$stateProvider
.state("root", {
url: "/",
// Any service can be injected into this controller.
// You can also define the controller separately and use
// "controller: "<NameOfController>" to reference it.
controller: function(History){
// History.header factory
History.pages.push(History.currentPage);
History.currentPage = "/";
}
})
.state("search", {
url: "/search",
controller: function(History, $routeParams) {
History.lastSearch = $routeParams
}
});
app.factory('<FactoryName>',function(){
var serviceObjectSingleton = {
pages: []
currentPage: ""
lastSearch: {}
}
return serviceObjectSingleton
})
If you're wondering what the difference between $routeProvider and $stateProvider is, it's just that $stateProvider has more features, mainly nested states and views... I think.
The easiest way is using cookies, angularjs provides a wrapping service for that.
Simply when you go to "/search" save your current URL parameters with "$cookieStore.put()" and once you've back you've got what you need with "$cookieStore.get()".
See the documentation at angularjs cookie store
I made a locationState service, you simply give it the values you want to persist and it stores them in the URL. So you can store all the state you want across all routes in your app.
Use it like this:
angular.module('yourapp')
.controller('YourCtrl', function ($scope, locationState) {
var size = locationState.get('size');
;
// ... init your scope here
if (size) {
$scope.size = size;
}
// ...and watch for changes
$scope.$watch('size', locationState.setter('size'));
}
Here's the code:
// Store state in the url search string, JSON encoded per var
// This usurps the search string so don't use it for anything else
// Simple get()/set() semantics
// Also provides a setter that you can feed to $watch
angular.module('yourapp')
.service('locationState', function ($location, $rootScope) {
var searchVars = $location.search()
, state = {}
, key
, value
, dateVal
;
// Parse search string
for (var k in searchVars) {
key = decodeURIComponent(k);
try {
value = JSON.parse(decodeURIComponent(searchVars[k]));
} catch (e) {
// ignore this key+value
continue;
}
// If it smells like a date, parse it
if (/[0-9T:.-]{23}Z/.test(value)) {
dateVal = new Date(value);
// Annoying way to test for valid date
if (!isNaN(dateVal.getTime())) {
value = dateVal;
}
}
state[key] = value;
}
$rootScope.$on('$routeChangeSuccess', function() {
$location.search(searchVars);
});
this.get = function (key) {
return state[key];
};
this.set = function (key, value) {
state[key] = value;
searchVars[encodeURIComponent(key)] = JSON.stringify(value);
// TODO verify that all the URI encoding etc works. Is there a mock $location?
$location.search(searchVars);
};
this.setter = function (key) {
var _this = this;
return function (value) {
_this.set(key, value);
};
};
});