I'm working on an Angular app, where I'm running into mostly the same problem as in this post:
AngularJS App: Load data from JSON once and use it in several controllers
I've got a factory that reads a JSON file, and returns the whole data object. Each controller, then, uses this factory (as a service?) to obtain the data, but then each controller has to pick it apart on its own. The JSON has to be searched and processed to get the relevant payload like, $scope.currentArray = data.someThing.allItems[i]; etc, and I obviously don't want to repeat this code in all the controllers.
Seems to me I can either find some way to share the data, after, say, MainController (the "first one") has finished working it, or I can add some new module "between" the controllers and the factory. This new module -- let's call it myProcessService? -- would then be the one getting the data object from the factory, and do all the processing there... once and for all. Then, each controller would only deal with myProcessService to (somehow) get the ready-formatted variables and arrays etc onto their respective scopes (yes, this is Angular 1).
If I try to give an example of how I'm doing this so far, maybe someone can help me with the necessary improvements? And, I am aware that it is a good idea to begin using the Angular 2 patterns already today, but please understand that I am first trying to get some grasp of how A1 works, before delving into A2 :)
var app = angular.module('myApp', ['ngRoute']);
app.factory('getDataFile', ['$http', function($http) {
function getStream(pid) {
return $http.get("data/" + pid + ".json")
.success(function(data) {
console.info("Found data for pid " + pid);
return data;
})
.error(function(err) {
console.error("Cant find data for pid " + pid);
return err;
});
}
return {getStream: getStream};
}]);
app.controller('MainController', ['$scope', 'getDataFile',
function($scope, getDataFile) {
getDataFile.getStream('10101011').success(function(data) {
// process "data" into what's relevant:
var i = getRelevantIndexForToday(new Date());
$scope.myVar = data.someField;
$scope.currentArray = data.someThing.allItems[i];
// etc... you get the drift
}
}]);
app.controller('SecondController', ['$scope', 'getDataFile',
function($scope, getDataFile) {
getDataFile.getStream('10101011').success(function(data) {
// process "data" into what's relevant:
var i = getRelevantIndexForToday(new Date());
$scope.myVar = data.someField;
$scope.currentArray = data.someThing.allItems[i];
// etc... you get the drift
}
}]);
Edit:
My ngRouter is set up something like this. They fill the ng-view div in my index.html. However -- and maybe this is frowned upon? -- I've also got a "MainController" which sits directly in the index.html body tag, such that I can show some data (from the back end) in the header part of the single page application.
app.config(function($routeProvider) {
$routeProvider
.when('/:id/work/:page_id', {
controller: 'AssetController',
templateUrl: 'app/views/work.html'
})
.when('/:id/work/', {
redirectTo: '/:id/work/1'
})
.when('/:id/', {
controller: 'DashController',
templateUrl: 'app/views/dashboard.html'
})
.otherwise({
redirectTo: '/'
});
});
and index.html is a lot like this:
<body ng-app="myApp">
<div class="container" ng-controller="MainController">
<h1>Welcome, {{username}}</h1>
<div ng-view></div>
</div>
</body>
You can add another helper function in your factory, that returns the required object that you want to share between controllers.
app.factory('getDataFile', ['$http', function($http) {
function getStream(pid) {
return $http.get("data/" + pid + ".json")
.success(function(data) {
console.info("Found data for pid " + pid);
return data;
})
.error(function(err) {
console.error("Cant find data for pid " + pid);
return err;
});
}
function getCurrent(pid) {
return getStream(pid).then(function() {
var i = getRelevantIndexForToday(new Date());
return {
myVar: data.someField,
currentArray: data.someThing.allItems[i];
};
});
}
return {
getStream: getStream,
getCurrent: getCurrent
};
}]);
app.controller('MainController', ['$scope', 'getDataFile',
function($scope, getDataFile) {
getDataFile.getCurrent('10101011').success(function(data) {
$scope.myVar = data.myVar;
$scope.currentArray = data.currentArray;
// etc... you get the drift
}
}]);
app.controller('SecondController', ['$scope', 'current',
function($scope, current) {
.success(function(data) {
$scope.myVar = data.myVar;
$scope.currentArray = data.currentArray;
}
}]);
Suggestion:
Also I suggest you to use resolve which allows you to pass data to your controller from your route.
Route:
.when('/:id/work', {
controller: 'AssetController',
templateUrl: 'app/views/work.html',
resolve: {
// you are injecting current variable in the controller with the data. You can inject this to each of your controller. you dont need to add the whole function in your next route. Just use current
current: function(getDataFile){
return getDataFile.getCurrent('10101011');
}
})
Controller:
app.controller('MainController', ['$scope', 'current',
function($scope, current) {
$scope.myVar = current.myVar;
$scope.currentArray = current.currentArray;
}]);
app.controller('SecondController', ['$scope', 'current',
function($scope, current) {
$scope.myVar = current.myVar;
$scope.currentArray = current.currentArray;
}]);
Now that you have
Thanks for giving an answer in line with my use of deprecated methods success and error, Subash. However, I had some problems with the code in your answer, and I got some help on #angularjs, so I thought I should post the updated code here.
var myApp = angular.module('myApp',[]);
myApp.factory('getDataFile', ['$http', function($http) {
function getStream(pid) {
// here, using placeholder data URL, just to get some data:
return $http.get('http://jsonplaceholder.typicode.com/users')
.then(function(result) {
console.info("Found data for pid " + pid);
return result.data;
})
.catch(function(err) {
console.error("Cant find data for pid " + pid);
return err;
});
}
function getCurrent(pid) {
return getStream(pid).then(function(data) {
var i = 1; // test
console.log("myVar = ", data[i].name);
return {
myVar: data[i].name
};
});
}
return {
getStream: getStream,
getCurrent: getCurrent
};
}]);
myApp.controller('MainController', ['$scope', 'getDataFile',
function($scope, getDataFile) {
$scope.name = "j";
getDataFile.getCurrent('10101011').then(function(user) {
$scope.myVar = user.myVar;
console.log("controller. myVar = ", user);
// etc... you get the drift
});
}]);
Related
I have an angular app with three views. When it loads it runs some code to populate the $scope variables. When I change views and then go back to the controller I want the initial code to run again but it doesn't. It seems it is cached and the $scope variables are not updated based on what happened.
How can I force the controller to run the initialisation code every time the view is loaded?
My routes:
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeController',
templateUrl: 'home.html'
})
.when('/teach', {
controller: 'TeachController',
templateUrl: 'teach.html'
})
.otherwise({
redirectTo: '/'
});
});
The code I want to run every time the '/' route is clicked:
getSubPools.success(function(data) {
$scope.userPools = data;
});
Controller in full:
app.controller('HomeController', ['$scope', '$filter', 'stream', 'removeDroplet', 'qrecords', 'helps', 'get_user', 'updateRecords', 'getSubPools', function($scope, $filter, stream, removeDroplet, qrecords, helps, get_user, updateRecords, getSubPools) {
get_user.success(function(data) { //get current user
$scope.user = data;
});
getSubPools.success(function(data) {
$scope.userPools = data;
});
stream.success(function(data) {
$scope.stream = data;
if ($scope.stream.length === 0) { //determine if user has stream
$scope.noStream = true;
} else {
$scope.noStream = false;
}
$scope.getNumberReady(); //determine if any droplets are ready
if ($scope.numberReady === 0){
$scope.noneReady = true;
} else {
$scope.noneReady = false;
$scope.stream = $filter('orderBy')($scope.stream, 'next_ready'); //orders droplets by next ready
}
});
$scope.showEditStream = true;
$scope.showStream = false;
$scope.rightAnswer = false;
$scope.wrongAnswer = true;
$scope.noneReady = false;
$scope.subbedDroplets = [];
$scope.focusInput = false;
}]);
You can use the $routeChangeStart and $routeChangeSuccess events to reload the data into the controller:
https://docs.angularjs.org/api/ngRoute/service/$route
Edit:
As mohan said as this will work for every route change, you can make a service to catch these events and for each route broadcast a special event.
And in the relevant controller/service listen to this event and reload data
If you want to force reload, then add an click function like follows,
Note: This will work only if you use $stateProvider
Home
and in controller ,
$scope.goToHome = function(){
$state.transitionTo('home', {}, {reload:true});
}
The issue here was that on clicking the link to '/' not all of the initialisation code was rerunning. Rather than making calls to the database to get fresh data, angular was just returning old data. The way I fixed this was to rewrite my factories. The factories that were failing were written:
app.factory('stream', ['$http', function($http) {
return $http.get('/stream/')
.success(function(data) {
return data;
})
.error(function(err) {
return err;
});
}]);
The factory that worked every time was written:
app.factory('stream', ['$http', function($http) {
return {
fetch: function () {
return $http.get('/stream/');
}
}
}]);
Now it runs every time. I am not sure why though.
I'm working on a mobile app using AngularJS as a framework, currently I have a structure similar to this:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/', {
templateUrl : 'pages/home.html',
controller : 'homeCtrl'
})
.when('/one', {
templateUrl : 'pages/one.html',
controller : 'oneCtrl'
})
.when('/two', {
templateUrl : 'pages/two.html',
controller : 'twoCtrl'
});
}]);
app.controller('homeCtrl', ['$scope', function($scope) {
}]);
app.controller('oneCtrl', ['$scope', function($scope) {
}]);
app.controller('twoCtrl', ['$scope', function($scope) {
}]);
And then I'm displaying the content with an ng-view:
<div class="ng-view></div>
Things are working well but I need to load data from a JSON file to populate all the content of the app. What I want is to make and an AJAX call only once and then pass the data through all my different controllers. In my first attempt, I thought to create a Service with an $http.get() inside of it and include that in every controller, but it does not work because it makes a different ajax request everytime I inject and use the service. Since I'm new using angular I'm wondering what is the best way or the more "angular way" to achieve this without messing it up.
Edit: I'm adding the code of the service, which is just a simple $http.get request:
app.service('Data', ['$http', function($http) {
this.get = function() {
$http.get('data.json')
.success(function(result) {
return result;
})
}
});
Initialize the promise once, and return a reference to it:
No need to initialize another promise. $http returns one.
Just tack a .then() call on your promise to modify the result
angular.module('app', [])
.service('service', function($http){
this.promise = null;
function makeRequest() {
return $http.get('http://jsonplaceholder.typicode.com/posts/1')
.then(function(resp){
return resp.data;
});
}
this.getPromise = function(update){
if (update || !this.promise) {
this.promise = makeRequest();
}
return this.promise;
}
})
Codepen example
Edit: you may consider using $http cache instead. It can achieve the same results. From the docs:
If multiple identical requests are made using the same cache, which is not yet populated, one request will be made to the server and remaining requests will return the same response.
Try this to get JSON Data from a GET Link:
(function (app) {
'use strict';
app.factory('myService', MyService);
MyService.$inject = ['$q', '$http'];
function MyService($q, $http) {
var data;
var service = {
getData: getData
};
return service;
//////////////////////////////////////
function getData(refresh) {
if (refresh || !data) {
return $http.get('your_source').then(function(data){
this.data = data;
return data;
})
}
else {
var deferrer = $q.defer();
deferrer.resolve(data);
return deferrer.promise;
}
}
}
}(angular.module('app')));
Now you can add this dependency in your controller file and use:
myService.getData().then(function(data){
//use data here
}, function(err){
//Handle error here
});
I'm working through this tutorial on creating a single-page MEAN stack todo app. I'm on this step, specifically. The tutorial covers modularization of code, and while I was able to separate my backend code (Express, Mongo, etc.) into modules successfully, when I separate my angular.js code, the todo app ceases to function. The specific error which is thrown to the console is "Uncaught Error: [$injector:modulerr]." The specific error is "nomod" (i.e. the module "simpleTodo" is failing to load.) I'd appreciate any help.
Code as one file (core.js):
var simpleTodo = angular.module('simpleTodo', []);
simpleTodo.controller('mainController', ['$scope', '$http', function($scope, $http) {
$scope.formData = {};
$http.get('/api/todos')
.success(function(data) {
$scope.todos = data;
})
.error(function(data) {
console.log('Error: ' + data);
});
$scope.createTodo = function() {
$http.post('/api/todos', $scope.formData)
.success(function(data) {
$scope.formData = {};
$scope.todos = data;
})
.error(function(data) {
console.log('Error: ' + data);
});
};
$scope.deleteTodo = function(id) {
$http.delete('/api/todos/' + id)
.success(function(data) {
$scope.todos = data;
})
.error(function(data) {
console.log('Error: ' + data);
});
};
}]);
Code in modules:
New core.js:
var simpleTodo = angular.module('simpleTodo',['todoController', 'todoService']);
Create/Delete Todo Service (todos.js):
angular.module('todoService', [])
.factory('Todos', ['$http', function($http) {
return {
get: function() {
return $http.get('/api/todos');
},
create: function(todoData) {
return $http.post('/api/todos', todoData);
},
delete: function(id) {
return $http.delete('/api/todos/' + id);
}
}
}]);
Controller file (main.js)
angular.module('todoController', [])
.controller('mainController', ['$scope', '$http', 'Todos', function($scope, $http, Todos) {
$scope.formData = {};
Todos.get()
.success(function(data) {
$scope.todos = data;
});
$scope.createTodo = function() {
if ($scope.formData !== null) {
Todos.create($scope.formData)
.success(function(data) {
$scope.formData = {};
$scope.todos = data;
});
}
};
$scope.deleteTodo = function(id) {
Todos.delete(id)
.success(function(data) {
$scope.todos = data;
});
};
}]);
Order of script loading on index.html:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.13/angular.min.js"></script>
<script src="js/controllers/main.js"></script>
<script src="js/services/todos.js"</script>
<script src="js/core.js"></script>
Thanks!
New info: After following floribon's advice, I get the same error, except instead of "simpleTodo" failing to load, it is "todoController" that cannot be loaded. I appreciate his advice but it still isn't working. :( Here's a github repo with his changes implemented, if you want to see: https://github.com/LeungEnterprises/simpleTodo
Since your controller needs to resolve the Todos dependency, you need to add the service todoService it to its module dependencies:
angular.module('todoController', ['todoService'])
Also, you will need to load the todos.js file before the main.js one (sicne it requires the former)
I perused my files and after extensive experimentation I deduced that the problem was being caused in the HTML file. The problem was an unclosed script tag.
However, I do appreciate everyone's help, especially #floribon!
I'm trying to get a specific product by its id from a JSON file with products. I have some kind of problem as this question
AngularJS : get back data from a json array with an id
the code is similar. I read through that question and the accepted answer there, still can't figured this out. From what I understand the $scope.search() returns a promise which after success triggers the .then() to set get the correct person.
This line of code prints out the products array and also getting the product id from the url.
However it prints out twice in the console.
console.log($scope.products + $routeParams.productId);
app.js
var app = angular.module('gtbApp', [
'ngRoute',
'productControllers'
]);
// Setting up the routes with right controllers and partials
app.config(['$routeProvider', function($routeProvider){
$routeProvider
.when('/main', {
templateUrl: 'partials/product-grid.html',
controller: 'ProductController'
})
.when('/product/:productId', {
templateUrl: 'partials/product-detail.html',
controller: 'ProductDetailCtrl'
})
.otherwise({
redirectTo: '/main'
});
}]);
controllers.js
var app = angular.module('productControllers', []);
// For product-grid.html
app.controller('ProductController', ['$http', function($http){
var store = this;
store.products = [];
$http.get('products.json').success(function(data){
store.products = data;
});
}]);
// For product-detail.html
app.controller('ProductDetailCtrl', ['$scope', '$routeParams', '$http', function($scope, $routeParams, $http){
$scope.search = function() {
var url = 'products.json';
// Return a promise object
return $http.get(url).success(httpSuccess).error(function(){
console.log('Unable to retrieve info form JSON file.');
});
}
httpSuccess = function(response) {
$scope.products = response;
}
function getById(arr, id) {
for (var i = 0, len = arr.length; i < len; i++) {
if (arr[i].id === id) {
return arr[i];
}
}
}
$scope.search().then(function(){
// Prints out the products array and id twice
console.log($scope.products + $routeParams.productId);
$scope.product = getById($scope.products, $routeParams.productId);
// Prints out twice "undefined"
console.log($scope.product);
});
}]);
The main question is how to get specific product based on id why in "ProductDetailCtrl"
$scope.product = getById($scope.products, $routeParams.productId);
doesn't work.
Thanks in advance!
Update:
Found out why $scope.product is undefined, it is just because the $routeParams.productId is a string, and in getById() need a integer in second args.
However I don't know why console.log($scope.product); prints out twice.
I don't really understand what your main question is here. But anyways. When you use the $http service it will return a promise, which you eventually will have to unwrap. What you are doing in your code is that you are unwrapping it twice. Which is fine.
With $http response you can either use 'success'/'error' or just 'then' which can take a success and an error callback. Which means you could either unwrap in the search function or after you call the search function.
$scope.search = function() {
var url = 'products.json';
$http.get(url)
.success(function(data){
$scope.product = getById($scope.products, $routeParams.productId);
})
.error(function() {
console.log('Unable to retrieve info form JSON file.');
});
}
You could also do something like:
$scope.search = function() {
var url = 'products.json';
return $http.get(url);
}
$scope.search().then(function(data) {
$scope.product = getById(data, $routeParams.productId);
}, errorCallback);
And the below would achieve the same result
$scope.search = function() {
var url = 'products.json';
return $http.get(url);
}
$scope.search()
.success(function(data) {
$scope.product = getById(data, $routeParams.productId);
})
.error(errorCallback);
or reference the promise:
$scope.search = function() {
var url = 'products.json';
return $http.get(url);
}
var dataPromise = $scope.search();
dataPromise.then(function(data) {
$scope.product = getById(data, $routeParams.productId);
}, errorCallback);
What you need to know is that as long as you're returning something within a success/error/then function it will return a promise which you will have to unwrap in order to get the data.
You should be either using the .success() and .error() on the $http-promise or only then .then()
Do it like this:
app.controller('ProductController', ['$scope', '$routeParams', '$http', function($scope, $routeParams, $http){
$scope.search = function() {
var url = 'products.json';
// Return a promise object
return $http.get(url);
}
.....
$scope.search()
.success(function(data){ // --> data is the products.json
... // handle the successfull call
} );
.error(function(...) {
... // handle the error
} );
// or:
$scope.search().then(
function(data){ // --> data is the products.json
... // handle the successfull call
},
function(...) {
... // handle the error
});
}]);
I have the following controller that uses a service Customers to return customers. The problem is that its only executing the service the first time the controller is run. Looking at my server I see its only performing the get request the FIRST time the controller used(loading that view) if I change views and say add a customer and come back to the view that list customers its not updated because there was not another get request from the service.
.controller('searchCTRL', ['$scope', '$http', 'Customers', function($scope, $http, Customers) {
$scope.customers = Customers;
$scope.deleteCustomer = function(id) {
$http.delete('/api/customer/' + id)
.success(function(data) {
$scope.customers.data = data;
})
.error(function(data) {
console.log('Error: ' + data);
});
};
}])
and
.factory('Customers', function($http){
var Customers = {};
$http.get('/api/customer')
.success(function(data) {
Customers.data = data;
})
.error(function(data){
console.log('error: ' + data);
});
return Customers;
});
if I stay on the view and reload the page it gets the data like it should but any subsequent visits to the page no longer execute the get. Any help would be appreciated.
Angular .factory is a singleton so it will always only run once. Also, the $http call is async so you should be using promise in order to get the data to your controller. Try the following:
.factory('Customers', function($http, $q){
return function () {
var d = $q.defer();
$http.get('/api/customer')
.success(function(data) {
d.resolve(data);
})
.error(function(data){
console.log('error: ' + data);
d.reject(data);
});
return d.promise;
};
});
and in your controller:
.controller('searchCTRL', ['$scope', '$http', 'Customers', function($scope, $http, Customers) {
Customers().then(function (data) {
$scope.customers = data;
});
...
As $http returns a promise, you can further simply your .factory by doing:
.factory('Customers', function($http){
return function () {
return $http.get('/api/customer');
};
})
For more detail, see documentation for $http.