How could I implement a fuzzy time date replacer using Knockout?
Asked Answered
S

2

12

I'd like to implement what this plugin does using jQuery: https://github.com/rmm5t/jquery-timeago

A short description of that plugin:

This will turn all abbr elements with a class of timeago and an ISO 8601 timestamp in the title (conforming to the datetime design pattern microformat):

<abbr class="timeago" title="2011-12-17T09:24:17Z">December 17, 2011</abbr>

Into something like this:

<abbr class="timeago" title="December 17, 2011">about 1 day ago</abbr>

Except using knockout my markup looks like this:

<abbr data-bind="attr: { title: Posted }" class="timeago"></abbr>

I think something isn't synced up because nothing is happening even if I put the call to timeago within the viewmodel itself. I'm guessing I need a subscriber that's attached to the observable "Posted" but I'm not sure how to set that up.

Sizing answered 29/6, 2012 at 21:9 Comment(0)
B
22

Your approach doesn't work, because timeago creates a cache via jQuery's data() function. Thus, simply updating the title isn't enough.

I think a custom binding is here the best and cleanest way to go:

ko.bindingHandlers.timeago = {
    update: function(element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor());
        var $this = $(element);

        // Set the title attribute to the new value = timestamp
        $this.attr('title', value);

        // If timeago has already been applied to this node, don't reapply it -
        // since timeago isn't really flexible (it doesn't provide a public
        // remove() or refresh() method) we need to do everything by ourselves.
        if ($this.data('timeago')) {
            var datetime = $.timeago.datetime($this);
            var distance = (new Date().getTime() - datetime.getTime());
            var inWords = $.timeago.inWords(distance);

            // Update cache and displayed text..
            $this.data('timeago', { 'datetime': datetime });
            $this.text(inWords);
        } else {
            // timeago hasn't been applied to this node -> we do that now!
            $this.timeago();
        }
    }
};

Usage is as simple as this:

<abbr data-bind="timeago: Posted"></abbr>

Demo: http://jsfiddle.net/APRGc/1/

Bello answered 30/6, 2012 at 0:32 Comment(4)
Will try this here in a few minutes, that was the route that I started down, but wasn't sure. Thanks for the answer.Sizing
Replace the line $this.attr('title', value); with $this.attr($.timeago.isTime($this) ? 'datetime' : 'title', value); so it will work with <time>Silverside
It should be noted that timeago only accepts ISO time strings (github.com/rmm5t/jquery-timeago/issues/76) so be sure to call .toISOString() on your date object.Juice
timeago now has an updateFromDOM method that can be used to simplify this code now. It seems others were trying to get timeago working with knockout binding - see: github.com/rmm5t/jquery-timeago/pull/132Prosenchyma
W
1

Here is an alternative, that really probably doesn't have any advantage over Niko's answer at all, except perhaps being a touch simpler :) --

usf.ko.bindings['timeago'] = {
  update: function(element, valueAccessor) {
    var $element, date;
    date = ko.utils.unwrapObservable(valueAccessor());
    $element = $(element);
    if (date) {
      $element.attr('title', date.toISOString());
      $element.data('tiemago', false);
      return $element.timeago();
    }
  }
};
Walterwalters answered 5/7, 2012 at 23:2 Comment(2)
This solution has one main disadvantage: timeago('refresh') means nothing different than timeago(), because there is no public "refresh" method. This does essentially mean that every time the observable is updated, a new timer is created with "setInterval" to manage the auto-refresh. So you have a potential memory-leak here.Bello
@Niko: The 'refresh' was a typo (wishful thinking?). :) You're right about the setInterval being a potential memory leak. My proposed code should not be used for that reason.Walterwalters

© 2022 - 2024 — McMap. All rights reserved.