The pleasantness of Om

With the aim of learning and practicing the art of Om, I’ve refactored a display component used on Etsy-fu’s website, namely the testimonials ticker.

The testimonials are stored in a vector of maps, where a single map might look like this:

{:shop_name "MomentsOfArt"
 :shop_owner "Loribeth"
 :avatar "http://img0.etsystatic.com/011/0/30011619/iusa_75x75.22273684_cjcb.jpg"
 :quote "I think you are providing a wonderful app to Etsy shop owners."}

The ticker doesn’t just pick a random item out of the collection. Instead, all items in the collection get shuffled and are displayed in rotation. After each and every one of them has been shown, the collection is reshuffled. That logic is embodied in the transition function. It is a one-liner.

(def app-state (atom {:testimonials (shuffle testimonials)}))

(defn transition [state]
  (om/transact! state :testimonials #(if (seq (pop %)) (pop %) (shuffle testimonials))))

(defn testimonial [state]
  (peek (:testimonials state)))

(defn widget [data owner]
  (reify
    om/IRender
    (render [this]
      (html [:div {:class "media"}
             [:a {:class "pull-left" :href "#"}
              [:img {:class "media-object avatar" 
                     :alt "Avatar" 
                     :style #js {:width "75px" :height "75px"}
                     :src (:avatar (testimonial data))}]]
             [:div {:class "media-body"}
              [:h4 {:class "media-heading"}
               [:a {:href (str "http://www.etsy.com/shop/" (:shop_name (testimonial data))) 
                    :class "shop" 
                    :target "_blank"} 
                (:shop_name (testimonial data))]]
              [:blockquote 
               [:p {:class "text-info quote"} 
                (:quote (testimonial data))]
               [:small 
                [:cite {:title "Source title" 
                        :class "shop_owner"} 
                 (:shop_owner (testimonial data))]]]]]))
    om/IDidMount
    (did-mount [this node]
      (js/setInterval transition 5000 data))))

(om/root 
 widget
 app-state
 {:target (sel1 :#content)})

Compare this to the imperative code in the original JavaScript version. Now testimonials is a an array of JavaScript objects (JSON), where a single object might look like this:

{shop_name: "MomentsOfArt",
 shop_owner: "Loribeth",
 avatar: "http://img0.etsystatic.com/011/0/30011619/iusa_75x75.22273684_cjcb.jpg",
 quote: "I think you are providing a wonderful app to Etsy shop owners."}

We’re starting by defining some global helper functions, followed by the logic to rotate and update the ticker.

(function() {
  (function() {
    var _base;
    return (_base = Array.prototype).shuffle != null ? (_base = Array.prototype).shuffle : _base.shuffle = function() {
      var i, j, _i, _ref, _ref1;
      for (i = _i = _ref = this.length - 1; _ref <= 1 ? _i <= 1 : _i >= 1; i = _ref <= 1 ? ++_i : --_i) {
        j = Math.floor(Math.random() * (i + 1));
        _ref1 = [this[j], this[i]], this[i] = _ref1[0], this[j] = _ref1[1];
      }
      return this;
    };
  })();

  window.delay = function(s, func) {
    return setInterval(func, s * 1000);
  };

  window.randomInt = function(lower, upper) {
    var start, _ref, _ref1;
    if (upper == null) {
      upper = 0;
    }
    start = Math.random();
    if (lower == null) {
      _ref = [0, lower], lower = _ref[0], upper = _ref[1];
    }
    if (lower > upper) {
      _ref1 = [upper, lower], lower = _ref1[0], upper = _ref1[1];
    }
    return Math.floor(start * (upper - lower + 1) + lower);
  };

  $(document).ready(function() {
    var rotate, testimonials, update;    
    rotate = (function() {
      var counter, increment;
      counter = 0;
      increment = function() {
        ++counter;
        if (counter === testimonials.length) {
          testimonials.shuffle();
          return counter = 0;
        }
      };
      return {
        value: function() {
          increment();
          return update(testimonials[counter]);
        }
      };
    })();
    update = function(testimonial) {
      return $("#testimonials .media").fadeOut(function() {
        $(".shop", $(this)).text(testimonial.shop_name);
        $(".shop", $(this)).attr("href", "http://www.etsy.com/shop/" + testimonial.shop_name);
        $(".quote", $(this)).text(testimonial.quote);
        $(".shop_owner", $(this)).text(testimonial.shop_owner);
        return $(".avatar", $(this)).attr("src", testimonial.avatar);
      }).fadeIn();
    };
    testimonials.shuffle();
    rotate.value();
    return delay(5, rotate.value);
  });

You might have recognized the module pattern in the rotate function, a technique allowing us to emulate private methods using closures. (The public function value shares an environment with the private function increment and the private variable counter.)

Is it a rhetorical question were I to ask which of the two versions, Om + Clojurescript vs vanilla JavaScript, looks more pleasant?

P.S. Follow me on Twitter.

Daniel Szmulewicz 28 February 2014
blog comments powered by Disqus