11

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).

Corbin Lewis
  • 380
  • 3
  • 5
  • 15

5 Answers5

17

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>
Veikko Karsikko
  • 3,671
  • 1
  • 20
  • 15
13

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.
Community
  • 1
  • 1
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • I'll spend some time putting this into my implementation. My current implementation isn't that CPU intensive from my observation, and I think there's only one timer for the entire page since that one timer calls an apply, but I'm certain this would be better. Thank you. – Corbin Lewis Aug 08 '13 at 16:59
  • @CorbinLewis, sorry I wasn't clear about the multiple timers... I was thinking of an alternate implementation where someone might use $timeout in a directive -- that would result in a separate timer for every timestamp. I agree your implementation only has one timer. – Mark Rajcok Aug 08 '13 at 17:06
4

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 ;-)

jandersen
  • 3,551
  • 22
  • 23
  • 1
    Your link to [`$interval`](https://docs.angularjs.org/api/ng/service/$interval) is correct but the link text says "$timeout" – jandersen Jun 11 '14 at 00:16
  • Note that angular also provides [$interval](https://docs.angularjs.org/api/ng/service/$interval) for repeatedly calling functions. – Wilfred Hughes Jun 11 '14 at 09:21
0

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.

Corbin Lewis
  • 380
  • 3
  • 5
  • 15
0

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.

jcamelis
  • 1,289
  • 1
  • 9
  • 10