I have a large list of items which I navigate to with a button. If I load the data for the list controller immediately, there's a lag while angular builds the list before you see any change to the page. If I instead do a setTimeout in the list controller that waits say, 50ms, the screen changes instantly and the button feels more responsive.
My question is, what is the "proper" way to do this, because I'm pretty sure a setTimeout is a huge hack here.
Edited with Plunkr: http://plnkr.co/edit/VqEiiAFZfV1hoXadw0vf?p=preview
Also, some code from plunkr because stack overflow requires it for some reason:
Test.controller('ListController', function($scope) {
var arr = [];
for (var i=0; i<5000; i++) {
arr[i] = i;
}
$scope.list = arr
});
Test.controller('ListThatWaitsController', function($scope) {
var arr = [];
for (var i=0; i<5000; i++) {
arr[i] = i;
}
setTimeout(function() {
$scope.list = arr;
$scope.$apply()
}, 10);
});
Use the resolve property on your routes (assuming you are using $routeProvider)
$routeProvider.when('/myroute', {
templateUrl: 'view/partial.html',
resolve: {
listData: ['service', function (service) {
return service.getList(); // Returns a promise that resolves with list
}]
}
});
This way in whatever controller manages your $route, you can inject listData and it will be the array you want, but the $route won't resolve until the data is ready to go.
Related
I'm trying to pull data from an external JSON file and display it for the user to see. Through various actions, the user would then be able to change the data returned from the JSON file, without writing those changes to the file (in this example, incrementing values by one by clicking on a div). I've created a promise service that successfully pulls the data and displays it. I can even get it so the data can be changed in individual controllers.
This is where I get stuck: I cannot find a way to make any changes to the data in the PromiseService, so changes cannot propagate globally. How do I make it that any change in the promise data at the controller level will be reflected in the PromiseService and, thus, reflected in any data binding in the app? I'm new to promises, so I'm open to a completely different approach.
Plunker
HTML:
<body ng-app="pageApp" ng-controller="pageCtrl" nd-model="items">
{{items}}
<div class="button" ng-controller="buttonCtrl" ng-click="incrementValues()">
Click to increment:
<br>{{items}}
</div>
</body>
PromiseService:
pageApp.factory('PromiseService', function($http) {
var getPromise = function() {
return $http.get('items.json').then(function(response) {
return response.data;
});
};
return {
getPromise: getPromise
};
});
Button Controller (Page Controller in Plunker):
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.incrementValues = function()
{
PromiseService.getPromise().then(function(data) {
$scope.items = data;
for(var i = 0; i < data.items.length; i++)
{
data.items[i]['value']++;
}
}).catch(function() {
});
};
});
The incrementValues function works successfully the first time, but each consecutive click re-pulls the promise and resets the data. To sum up: how do I reflect the incremented values in the PromiseService, as opposed to local variables?
You could add to your factory a private property where you store the items. Then create 3 different methods to update and access to that property.
pageApp.factory('PromiseService', function($http) {
var items = {}; // [] in case it is an array
var updateData = function(updatedData){
items = updatedData;
}
var getUpdateData = function(){
return items;
}
var getPromise = function() {
return $http.get('items.json').then(function(response) {
items = response.data;
return response.data;
});
};
return {
getPromise: getPromise,
updateData : updateData,
getUpdateData : getUpdateData
};
});
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.items = [];
//You should call this method to retrieve the data from the json file
$scope.getData = function(){
PromiseService.getPromise().then(function(data) {
$scope.items = data;
}).catch(function() {
});
}
$scope.incrementValues = function(){
for(var i = 0; i < $scope.items.length; i++){
$scope.items[i]['value']++;
}
PromiseService.updateData($scope.items); //This could be skipped in case you do not want to 'store' these changes.
};
});
Then in others controller you could use the same service to retrieve the updated Data like this:
$scope.items = PromiService.PromiseService();
In the future you could also create a new method to update the json itself instead of stored internally
Your function creates a new $http call every time it's called, and thus returns a new promise, encspsulating new data, every time it's called.
You need to return the same promise every time:
var thePromise = $http.get('items.json').then(function(response) {
return response.data;
});
var getPromise = function() {
return thePromise;
};
i'm trying to update a scope list inside a callback function. This apparently works fine, but, after some seconds, console gets error: [$rootScope:infdig]. I tried to disable two-way databinding, but, the error continues.
Controller:
app.controller('ChapterCtrl', function ($rootScope, $scope, Services, chapter) {
$rootScope.headerTitle = chapter.name;
$scope.terms = [];
cctdbterms.webdb.getTermsByChapter(chapter.id, function(tx, results) {
$scope.terms = results.rows;
$scope.$apply();
});
});
View:
<div class="view" ng-repeat="term in terms">
<div ng-bind-html="term.description"></div>
</div>
The answer that a find is seraching is: "Problem was that the filter was providing a different array each time hence causing a loop" from Why do I get an infdig error?
Thinking about it, I solved in a simple way, interating my returning list:
app.controller('ChapterCtrl', function ($rootScope, $scope, Services, chapter) {
$rootScope.headerTitle = chapter.name;
$scope.terms = [];
cctdbterms.webdb.getTermsByChapter(chapter.id, function(tx, results) {
for (var i = 0; i < results.rows.length; i++) {
var term = results.rows[i];
$scope.terms.push(term);
}
$scope.$apply();
});
});
I have an interesting situation, which I fully admit I may not be handling correctly.
I want to load directives in to an Angular app using AngularAMD, but the information on the directives is coming from a database/API call. Here's how I'm handing it at the moment:
app.run(function ($rootScope, $state, $http) {
$rootScope.$state = $state;
$rootScope.rootPages = JSON.parse(localStorage.pages);
$http.get('api/Directives').then(function (resp) {
for (var i = 0; i < resp.data.length; i++) {
var dirInfo = resp.data[i];
var dirConfig = JSON.parse(dirInfo.Configuration);
angularAMD.directive(dirInfo.Name, function () {
return {
restrict: 'E',
controller: eval(dirConfig.controller),
templateUrl: dirConfig.templateUrl,
scope: true,
}
});
}
}, function (error) {
alert('error');
});
});
At the moment this generates two directives, and I have two links on my page, one for each module (using angular-ui-router and router-ui-extras to load those based on another database table/API call). Theoretically clicking link 1 should bring up directive 1, and link 2 should bring up directive 2. However, when I click link 1, directive 2 comes up. It's almost as if the only directive that registers is directive 2.
Here's the information I get from the database:
dirInfo1: {
Name: directiveOne,
templateUrl: /html/directiveOne.html,
controller: controllerOne
}
dirInfo2: {
Name: directiveTwo,
templateUrl: /html/directiveTwo.html,
controller: controllerTwo
}
In this case, only directiveTwo.html is shown, and only controllerTwo fires.
Any help would be greatly appreciated.
It't a usual problem using for loop with delayed/async operations.
By the moment that first angularAMD.directive is resolved var dirInfo = resp.data[i]; inside of the loop is is equal to the last item in the list.
You need to use either Promises or anonymous function inside of a loop to keep the link to a right index inside of async function. Check simple example of what's happening in your case here: https://stackoverflow.com/a/13977142/405623
for ( i = 0; i < items.length; i++) {
(function(thisItem) {
setTimeout(function() {
console.log(thisItem);
}, i * 1000);
)(items[i]);
}
I'm building an app in angularjs, where I have a central notification queue. Any controller can push into the queue and digest the messages.
I have built a service like:
angular.module('app').factory('notificationSvc', ['translateSvc', notification]);
function notification(translate) {
var notificationQ = [];
var service = {
add: add,
getAll: getAll
};
return service;
function add(message, type) {
notificationQ.push({
message: message,
type: type
});
}
function getAll() {
return notificationQ;
}
}
(One of the problems with this is that the notificationQ can be modified unsafely by calling svc.getAll()[3].message = "I have changed a message"; or something similar. I originally wanted a "push only" service with immutable messages, but this problem is outside of the scope of this question.)
If I digest this queue in a controller like:
$scope.notifications = svc.getAll();
$scope.current= 0; // currently visible in the panel
And use it like:
<div ng-repeat="notification in notifications" ng-show="$index == current">
<p>{{notification.message}}</p>
</div>
I can bind to it, see it changing and all is well. I can cycle through past notifications by changing the variable current.
The question:
When the queue gets a new element I want the $scope.index variable to change to notifications.length - 1. How do I do that?
I have seen examples using $rootScope.$broadcast('notificationsChanged'); and $scope.$on('notificationsChanged', function() { $scope.index = $scope.notifications.length - 1; });, but I did not really like the pattern.
I have a controller that knows about the service, has a direct reference to it, and yet we use $rootScope to communicate? Everything else sees the $rootScope, and all the events from different services will clutter up there.
Can't I just put the event on the service instead? Something like this.$broadcast('notificationsChanged') in the service and svc.$on('notificationsChanged', function() { ... }); in the controller.
Or would it be cleaner to watch the data directly? If yes, how? I don't like this as I was not planning on exposing the full array directly (I was planning on get(index) methods) it just sort of happened along the lines where I had no idea what I was doing and was happy that at least something works.
You could just manage events yourself. For example (untested):
function EventManager() {
var subscribers = [];
var service = {
subscribe: subscribe;
unsubscribe: unsubscribe;
publish: publish
}
return service;
function subscribe(f) {
subscribers.push(f);
return function() { unsubscribe(f); };
}
function unsubscribe(f) {
var index = subscribers.indexOf(f);
if (index > -1)
subscribers.splice(index, 1);
}
function publish(e) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i](e);
}
}
}
function notification(translate) {
var notificationQ = [];
var addEvent = new EventManager();
var service = {
add: add,
getAll: getAll,
onAdded: addEvent.subscribe;
};
return service;
function add(message, type) {
var notification = {
message: message,
type: type
};
notificationQ.push(notification);
addEvent.publish(notification);
}
function getAll() {
return notificationQ;
}
}
Then, from your controller:
...
var unsubscribe = notificationSvc.onAdded(function(n) { /* update */ });
Caveat: using this method the service will maintain a reference to the subscriber function that is passed to it using subscribe, so you have to manage the subscription using $scope.$on('$destroy', unsubscribe)
The notification approach would definitely work. Depending on your implementation it would be the right solution.
Another approach would be to watch the notifications array in your controller, like this:
$scope.$watchCollection('notifications', function(newValue, oldValue) {
$scope.index = newValue.length - 1;
});
This should work, because your controller receives a direct reference to the notifications array and therefore can watch it directly for changes.
As runTarm pointed out in the comments, you could also directly $watch the length of the array. If you're only interested in length changes this would be a more memory saving approach (since you don't need to watch the whole collection):
$scope.$watch('notifications.length', function (newLength) {
$scope.index = newLength - 1;
});
I built an infinite scroll for a mobile web app built with AngularJS with the following extras features:
I built it to be bidirectional
This is for a mobile web app so I wanted it to unload out-of-view contents to avoid memory issues
Here is the jsfiddle link.
Now, I have a few questions and I also needs a small code review:
I am not familiar with promises, but then() seems to be executed before $digest. Thus, I need to delay my codes with $timeout. For me, it's a sign that something is wrong. I would like to remove the $timeout on lines 85 and 98. The $timeout on line 85 is a bit "hacky", I need to make sure it is executed ms after then() otherwise, it won't work and I don't know why.
I would like to know if it's considered a "good practice" to call a $scope method from a directive. In my code, I am calling $scope.init(value) from my directive.
Including jQuery for a position() is quite funny. Should I be using a services with a function that does what $.position() does?
I know those could be seperate questions but they are really related to my piece of code.
For those who do not want to click on the jsfiddle link, here is the code:
HTML:
<div id="fixed" scroll-watch="4" scroll-up="loadTop()" scroll-down="loadBottom()">
<ul>
<li data-id="{{i.id}}" ng-repeat="i in items" ng-class="calculateType(i.id)">{{i.id}}</li>
</ul>
</div>
JS:
function Main($scope, $timeout, $q) {
var cleanup = 5;
$scope.items = [];
//This is called from the scrollWatch directive. IMO, this shouldn't be a good idea
$scope.init = function(value) {
var deferred = $q.defer();
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
$scope.items = [{id: +value}];
$scope.loadTop();
$scope.loadBottom();
deferred.resolve();
}, 200);
return deferred.promise;
};
//This is only used to simulate different content's heights
$scope.calculateType = function(type) {
return 'type-' + Math.abs(type) % 4;
};
$scope.loadBottom = function() {
var deferred = $q.defer(),
counter;
if ($scope.items.length > 1) {
$scope.items.splice(0, cleanup);
}
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[$scope.items.length - 1]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.push({id: counter + i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
$scope.loadTop = function() {
var deferred = $q.defer(),
counter;
//Why can't I use this here?
//$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
//This $timeout is used to simulate an Ajax call so I will keep it there
$timeout(function() {
counter = (($scope.items[0]) || {id: 0}).id;
for (var i = 1; i < 6; i++) {
$scope.items.unshift({id: counter - i});
}
deferred.resolve();
}, 200);
return deferred.promise;
};
//Why is this method needs to be delayed inside the directive? I would like to call it in loadTop()
$scope.removeBottom = function() {
$scope.items.splice($scope.items.length-cleanup, $scope.items.length);
};
}
angular.module('scroll', []).directive('scrollWatch', ['$timeout', function($timeout) {
var lastScrollTop = 0;
return function($scope, elm, attr) {
var raw = elm[0];
$scope.init(attr.scrollWatch).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = $('li[data-id="' + attr.scrollWatch + '"]').position().top;
}, 300); //This value needs to be great enough so it is executed after the $scope.loadTop()'s resolve, for now, I know that I can set it to 300 but in real life app?
});
elm.bind('scroll', function() {
if (raw.scrollTop > lastScrollTop && raw.scrollTop + raw.offsetHeight >= raw.scrollHeight) {
$scope.$apply(attr.scrollDown);
} else if (raw.scrollTop < lastScrollTop && raw.scrollTop === 0) {
var scrollHeight = raw.scrollHeight;
$scope.$apply(attr.scrollUp).then(function() {
//Why do I need this? It looks like the resolve is called before the $digest cycle
$timeout(function() {
raw.scrollTop = raw.scrollHeight - scrollHeight;
//I would like to move this in the $scope.loadTop()
$scope.removeBottom();
});
});
}
lastScrollTop = raw.scrollTop;
});
};
}]);
Thank you
http://www.youtube.com/watch?v=o84ryzNp36Q
Is a great video on Promises, how to write them and how they work.
https://github.com/stackfull/angular-virtual-scroll
Is a directive replacement for ng-repeat that doesn't load anything not on screen It does from what I can tell exactly what your looking for.
I would have put this as a comment but you need 50 cred or reputation or whatever they call it.