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.
Related
I wan't to be able to change pages based on url. My url's look like this http://todolist.com/#/1 where last number is page number. So far my pagination is working (angular ui bootstrap). If i try to change page with numbers or buttons in pagination row the pages will change based on response. But url are not changing in url bar and if i change url manually the pages won't change.
This is my controller
controllers.todoCtrl = function ($scope, $timeout, todoFactory, $location, $routeParams) {
if($routeParams.pageNumber == undefined || $routeParams.pageNumber == null){
$scope.currentPage = 1;
} else {
$scope.currentPage = $routeParams.pageNumber;
}
getData();
//get another portions of data on page changed
$scope.pageChanged = function () {
getData();
};
/**
* Get list of todos with pagination
*/
function getData() {
todoFactory.index($scope.currentPage).then(function (data) {
$scope.totalItems = data.paging.count;
$scope.itemsPerPage = data.paging.limit;
$scope.todos = data.Todos;
});
}
My routes
app.config(function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'app/templates/todolists.html',
controller: 'todoCtrl'
}).when('/:pageNumber', {
templateUrl: 'app/templates/todolists.html',
controller: 'todoCtrl'
}).otherwise({ redirectTo: '/'});
What do i have to do to make pagination based on url working. If you need any additional information, please let me know and i will provide. Thank you
you can use the updateParams function of $route to update the url.
So your code would look like this:
//get another portions of data on page changed
$scope.pageChanged = function () {
//getData();
$route.updateParams({pageNumber: $scope.currentPage});
};
This will cause the url to change. However keep in mind that this will destroy and recreate your controller.
Personally I avoid using the build in Angular router and prefer to use UI-Router instead. UI-Router uses a state base approach with a nice clean interface
So in order to use UI-Router you have to grab it from here or install it with your favorite package manager.
Your routes would be configured like this:
var app = angular.module('myApp', ['ui.router']);
app.config(function($stateProvider) {
$stateProvider.state('home', {
url: '/',
templateUrl: 'app/templates/todolists.html',
controller: 'todoCtrl'
})
.state("details", {
url: '/:pageNumber',
templateUrl: 'app/templates/todolists.html',
controller: 'todoCtrl'
});
});
As you can see in the above sample, states get names with UI-Router. You can use those names later in your controllers and templates to reference the states. In addition to that you can have nested states.
Example in your controller:
controllers.todoCtrl = function ($scope, $timeout, todoFactory, $location, $state, $stateParams) {
if(!$stateParams.pageNumber){
$scope.currentPage = 1;
} else {
$scope.currentPage = $stateParams.pageNumber;
}
getData();
//get another portions of data on page changed
$scope.pageChanged = function () {
$state.go("details", {pageNumber: $scope.currentPage });
};
/**
* Get list of todos with pagination
*/
function getData() {
todoFactory.index($scope.currentPage).then(function (data) {
$scope.totalItems = data.paging.count;
$scope.itemsPerPage = data.paging.limit;
$scope.todos = data.Todos;
});
}
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
});
}]);
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
});
Lets say i list all users in a list, when i click a user i want to route to a new view and get the data for the selected person.
What is the preferred way? Should i move the data i already got when i listed the users or should i create a new server call?
My first thought is to pass the data, but the problem with this is that the data the gets lost if the user refreshes the page.
What is the best practice to solve this?
Small example:
(function() {
var app = angular.module('app');
var controllerId = 'app.controllers.views.userList';
app.controller(controllerId, [
'$scope', 'UserService',function ($scope, userService) {
var vm = this;
vm.users = [];
userService.getAllUsers().success(function (data) {
vm.users= data.users;
});
var gotoUser = function(user) {
// Pass the user to UserDetail view.
}
}
]);
})();
<div data-ng-repeat="user in vm.users" ng-click="vm.gotoUser(user)">
<span>{{customer.firstname}} {{customer.lastname}}</span>
</div>
i now list the user details in UserDetail view, this view is now vulnerable against a browser refresh.
Typically most people just create a new server call, but I'll assume you're worried about performance. In this case you could create a service that provides the data and caches it in local storage.
On controller load, the controller can fetch the data from the service given the route params and then load the content. This will achieve both the effect of working on page refresh, and not needing an extra network request
Here's a simple example from one of my apps, error handling left out for simplicity, so use with caution
angular.
module('alienstreamApp')
.service('api', ['$http', '$q','$window', function($http, $q, $window) {
//meta data request functions
this.trending = function() {
}
this.request = function(url,params) {
var differed = $q.defer();
var storage = $window.localStorage;
var value = JSON.parse(storage.getItem(url+params))
if(value) {
differed.resolve(value);
} else {
$http.get("http://api.alienstream.com/"+url+"/?"+params)
.success(function(result){
differed.resolve(result);
storage.setItem(url+params,JSON.stringify(result))
})
}
return differed.promise;
}
}]);
I would say that you should start off simple and do a new server call when you hit the new route. My experience is that this simplifies development and you can put your effort on optimizing performance (or user experience...) where you will need it the most.
Something like this:
angular.module('app', ['ngRoute', 'ngResource'])
.factory('Users', function ($resource) {
return $resource('/api/Users/:userid', { userid: '#id' }, {
query: { method: 'GET', params: { userid: '' }, isArray: true }
});
});
.controller("UsersController",
['$scope', 'Users',
function ($scope, Users) {
$scope.loading = true;
$scope.users = Users.query(function () {
$scope.loading = false;
});
}]);
.controller("UserController",
['$scope', '$routeParams', 'Users',
function ($scope, $routeParams, Users) {
$scope.loading = true;
$scope.user = Users.get({ userid: $routeParams.userid }, function () {
$scope.loading = false;
});
$scope.submit = function () {
$scope.user.$update(function () {
alert("Saved ok!");
});
}
}]);
.config(
['$routeProvider', '$locationProvider',
function ($routeProvider, $locationProvider) {
$routeProvider
.when('/users', {
templateUrl: '/users.html',
controller: 'UsersController'
})
.when('/users/:userid', {
templateUrl: '/user.html',
controller: 'UserController'
})
.otherwise({ redirectTo: '/users' });
}
]
);
When I change route, from say /set/1 to /set/2, then it still shows the information from /set/1 until I manually refresh the page, I've tried adding $route.refresh to the ng-click of the links to these pages, but that didn't work either. Any ideas?
Below is the routing code, this works fine, all routing is done via links, just <a> tags that href to the route.
angular.module('magicApp', ['ngRoute']).config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'pages/home.html'
}).when('/set', {
redirectTo: '/sets'
}).when('/set/:setID', {
controller: 'SetInformationController',
templateUrl: 'pages/set.html'
}).when('/card', {
redirectTo: '/cards'
}).when('/card/:cardID', {
controller: 'CardInformationController',
templateUrl: 'pages/card.html'
}).when('/sets', {
controller: 'SetListController',
templateUrl: 'pages/sets.html'
}).when('/cards', {
controller: 'CardListController',
templateUrl: 'pages/cards.html'
}).when('/search/:searchterm', {
controller: 'SearchController',
templateUrl: 'pages/search.html'
}).otherwise({
redirectTo: '/'
});
}]);
Below is the code for the SetListController, it uses the routeParams to grab the correct information from a service, which works, when I go to /set/1 then it returns the right information, if I then go back then go to /set/2 it still shows the information from set 1, until I refresh the page.
.controller('SetInformationController', function($scope, $routeParams, $route, SetInformationService, CardSetInformationService) {
$scope.set = [];
$scope.cards = [];
function init() {
SetInformationService.async($routeParams.setID).then(function(d) {
$scope.set = d;
});
CardSetInformationService.async($routeParams.setID).then(function(d) {
$scope.cards = d;
})
}
init();
})
The HTML itself has no reference to the controller, or anything like that, just the objects in the scope, namely set and cards.
I figured it out! The problem wasn't actually with the routing it was with my service, here was the service before:
.factory('SetInformationService', function($http) {
var promise;
var SetInformationService = {
async: function(id) {
if ( !promise ) {
// $http returns a promise, which has a then function, which also returns a promise
promise = $http.get('http://api.mtgdb.info/sets/' + id).then(function (response) {
// The then function here is an opportunity to modify the response
console.log("Set Information");
console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
}
// Return the promise to the controller
return promise;
}
};
return SetInformationService;
})
where it should have been:
.factory('SetInformationService', function($http) {
var promise;
var SetInformationService = {
async: function(id) {
// $http returns a promise, which has a then function, which also returns a promise
promise = $http.get('http://api.mtgdb.info/sets/' + id).then(function (response) {
// The then function here is an opportunity to modify the response
console.log("Set Information");
console.log(response);
// The return value gets picked up by the then in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return SetInformationService;
})