Updating "time ago" values in Angularjs and Momentjs - angularjs

Original: I have a table generated with ng-repeat with hundreds of entries consisting of several different unix timestamps. I'm using moment.js to make them display like "19 minutes ago" or however long ago it was. How would I have these update every five minutes, for example, without having to refresh the entire table (which takes a few seconds and will interrupt the user's sorting and selections).

I use the following filter. Filter updates value every 60 seconds.
angular
.module('myApp')
.filter('timeAgo', ['$interval', function ($interval){
// trigger digest every 60 seconds
$interval(function (){}, 60000);
function fromNowFilter(time){
return moment(time).fromNow();
}
fromNowFilter.$stateful = true;
return fromNowFilter;
}]);
And in html
<span>{{ myObject.created | timeAgo }}</span>

I believe filters are evaluated every digest cycle, so using a filter to display your "time ago" strings might get CPU-expensive with hundreds of entries.
I decided to go with a pubsub architecture, using essentially eburley's suggested approach (mainly because I don't have to watch for $destroy events and manually unsubscribe), but with a NotificationService rather than a "Channel" function:
.factory('NotificationService', ['$rootScope',
function($rootScope) {
// events:
var TIME_AGO_TICK = "e:timeAgo";
var timeAgoTick = function() {
$rootScope.$broadcast(TIME_AGO_TICK);
}
// every minute, publish/$broadcast a TIME_AGO_TICK event
setInterval(function() {
timeAgoTick();
$rootScope.$apply();
}, 1000 * 60);
return {
// publish
timeAgoTick: timeAgoTick,
// subscribe
onTimeAgo: function($scope, handler) {
$scope.$on(TIME_AGO_TICK, function() {
handler();
});
}
};
}])
A time-ago directive registers/subscribes a timestamp (post.dt in example HTML below) with the NotificationService:
<span time-ago="post.dt" class="time-ago"></span>
.directive('timeAgo', ['NotificationService',
function(NotificationService) {
return {
template: '<span>{{timeAgo}}</span>',
replace: true,
link: function(scope, element, attrs) {
var updateTime = function() {
scope.timeAgo = moment(scope.$eval(attrs.timeAgo)).fromNow();
}
NotificationService.onTimeAgo(scope, updateTime); // subscribe
updateTime();
}
}
}])
A few comments:
I tried to be efficient and not have the directive create a new scope. Although the directive adds a timeAgo property to the scope, in my usage, this is okay, since I only seem to use the time-ago directive inside an ng-repeat, so I'm just adding that property to the child scopes created by ng-repeat.
{{timeAgo}} will be examined every digest cycle, but this should be faster than running a filter
I also like this approach because I don't have a timer running on every timestamp. I have one timer running in the NotificationService.
Function timeAgoTick() could be made private, since likely only the NotificationService will need to publish the time ago event.

Use angular's $timeout service (just a wrapper around setTimeout()) to update your data. Note the third parameter which indicates Angular should run a $digest cycle that will update your data bindings.
Here's an example: http://jsfiddle.net/jandersen/vfpDR/
(This example updates every second so you don't have to wait 5 min to see it ;-)

I ended up having $scope.apply() be called every five minutes by setInterval which reapplies the filters formatting the timestamps into "x minutes ago". Maybe a bit hacky but it works. I'm certain it isn't the optimal solution.

I made a wrapper for momentjs https://github.com/jcamelis/angular-moment
<p moment-interval="5000">{{"2014-05-21T14:25:00Z" | momentFromNow}}</p>
I hope it works for you.

Related

AngularJS Why is this controller variable not getting updated in the UI?

I'm having a controller using StompJS to subscribe to a url (back-end is Spring Java) that returns an alternating string "list" and "box" every 5 seconds. I want to update my UI element when StompJS receives some data, but I couldn't get the UI element to update. I've test the same logic with a $timeout and the UI is getting updated so it must has something to do with the way callback function works. Can anyone see what is the reason UI is not updating?
I have these simple UI elements:
<input ng-model="ctrl.uniqueId"/>
<input ng-model="test"/>
ctrl.uniqueId is to verify whether the actual controller instance is being updated. For some reason, only 1 controller is making 5 different subscribes every time. If someone can help with that, it'd be great too but I doubt you can get much info unless you see all my codes setup.
Anyway, in my controller (tried self.test and it didn't work so I tried with $scope.test to see if it makes a difference):
self.uniqueId = window.performance.now();
$scope.test = 'list';
// the UI will be updated to dummy after 3 seconds.
$timeout(function() {
$scope.test="dummy";
}, 3000);
// the UI will not update.
var callBackFn = function(progress) {
$scope.test = progress;
console.log(self.uniqueId + ": " + $scope.test);
};
// the server returns alternating data (list and box) every 5 seconds
MyService.subscribeForUpdate('/topic/progressResults', callBackFn);
This is my service's code for StompJS if that matters:
self.subscribeForUpdate = function(channelUrl, callBackFn) {
self.socket.stomp.connect({}, function() {
self.socket.subscription = self.socket.stomp.subscribe(channelUrl,
function (result) {
//return angular.fromJson(result.body);
callBackFn(result.body);
return result.body;
}
);
});
};
This is console.log results:
1831.255000026431: list
1831.255000026431: box
Extra: is it possible to get the return data without callback function similar to Promise?
Be sure to use $apply:
app.service("myService", function($rootScope) {
var self = this;
self.subscribeForUpdate = function(channelUrl, callBackFn) {
self.socket.stomp.connect({}, function() {
self.socket.subscription = self.socket.stomp.subscribe(channelUrl,
function (result) {
//return angular.fromJson(result.body);
$rootScope.$apply(function() {
callBackFn(result.body);
});
return result.body;
}
);
});
};
})
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc... You can also use $apply() to enter the AngularJS execution context from JavaScript.
Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
For more information, see
AngularJS Developer Guide - Integration with the browser event loop
This is a very common issue and happens when a 3rd-party library(out of the angular environment) is used with angularjs. In such cases you need to manually trigger a digest cycle using the:
$scope.$apply()
After that all angular bindings will be updated. Using $timeout (even without timeValue) has the same result as it also triggers $apply()

Autosave form fields for every 15 sec in AgularJS without using $watch

I want to save a form field values every 15 sec with out using $watch and also it should stop executing once it has been moved to a different form. In the form I will be having many fields so I think $ watch will be having performance issue and also am not sure how to call all fields at once for %watch. So I decided to use $interval but I want to stop this execution once I moved to different controller or different form. If user comes back again to this form again this interval function should start automatically. please would you suggest me best way to handle this.
Use $interval like you are planning to do but assign it to a variable
with $scope.on('$destroy', function() { }); callback you can destroy the interval when switching to a different controller.
var intervalPromise = $interval(function() { // your code }, 15000);
$scope.$on('$destroy',function(){
if(intervalPromise)
$interval.cancel(intervalPromise);
});

Binding function to $interval

I have a service that I'm using to manage the state, like the time since a user last did something. In another directive, I use the interval service to check this state (lastActivity), but it's not getting updated:
// myservice
start() {
this.$document.on(this.DOMevents, this.updateActivity);
}
stop() {
this.$document.off(this.DOMevents, this.updateActivity)
}
updateActivity() {
this.lastActivity = new Date();
}
// other directive
$interval(() => {
console.log(myservice.lastActivity.getTime()); // lastActivity is just a javascript Date
}
}, 1000);
I think this is the closure problem in that $interval gets myservice before it runs every 1000 ms so the time is always the same. Is there a way to pass that in to get updated? I see in the docs https://docs.angularjs.org/api/ng/service/$interval
that there is a pass option as the last parameter, but since I'm using TypeScript and an older version of Angular, I won't be able to use this feature. Is there another way to do this?
Maybe this explains my problem clearer:
var now = new Date().getTime();
$interval(() => {
console.log(now);
}, 1000);
I have some service that is keeping track of time amongst other things. In another directive, after a certain amount of time has passed I want to check this value and do some logic. Problem is, now is always the same time. So in myservice, the lastActivity time is not getting read properly in the $interval function.

Watch for dom element in AngularJS

I'm looking for a pure angularJS way to call a controller method once a particular dom element is rendered. I'm implementing the scenario of a back button tap, so I need to scroll to a particular element once it is rendered. I'm using http://mobileangularui.com/docs/#scrollable.
Update: how my controller looks like:
$scope.item_ready=function(){
return document.getElementById($scope.item_dom_id).length;
};
$scope.$watch('item_ready', function(new_value, old_value, scope){
//run once on page load, and angular.element() is empty as the element is not yet rendered
});
Thanks
One hack that you could do and I emphasize hack here but sometimes it's just what you need is watch the DOM for changes and execute a function when the DOM hasn't changed for 500ms which is accepted as a fair value to say that the DOM has loaded. A code for this would look like the following:
// HACK: run this when the dom hasn't changed for 500ms logic
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
// run your code here
}, 10)
});
}
Read this post Calling a function when ng-repeat has finished
But don't look at the accepted answer, use the 3rd answer down by #Josep by using a filter to iterate through all your repeat items and call the function once the $last property returns true.
However instead of using $emit, run your function...This way you don't have to rely on $watch. Have used it and works like a charm...

Angular: Calling function that changes scope variable on interval

I've built a function that does some text transformations on a string. I'm calling this function from my view.
I would now like to call this function from a controller with a set interval,every 3 seconds.
I was trying with this in my controller:
$scope.MyTextFunction = function(input) {
cancelRefresh = $timeout(function myFunction(input) {
console.log(input);
// Do things here
cancelRefresh = $timeout(myFunction, 3000);
},3000);
};
I'm having some trouble getting this to work. Specifically, it seems all the functions that are called in the view are being triggered again. So console.log(input) gets called 4 times, then 8 times, then 16 times, etc, every 3 seconds. (I'm guessing this is due to the digest cycle? I'm not totally clear on this).
As you can expect, after staying on this view for 20 seconds the browser becomes unresponsive.
How can I make write a function that
triggers every 3 seconds,
is tied to the scope so I can use the output in the view and
doesn't trigger all the existing functions that already exist in the view?
I've set up a plunkr that contains the text transformation string: http://plnkr.co/edit/JlEBlCBG3roCqkiKegI6?p=preview

Resources