Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

wrapping knockout.js using clojurescript

I'm trying to wrap knockout.js in clojurescript but its turning to be very difficult. The problem that I'm having is the reference to the 'this' variable. I'm thinking of giving up and just using javascript directly.

I've taken examples off http://knockoutjs.com/examples/helloWorld.html and http://knockoutjs.com/examples/contactsEditor.html

I've managed to wrap easy functions with some macros. For example:

var ViewModel = function() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");

    this.fullName = ko.computed(function() {
        // Knockout tracks dependencies automatically. It knows that fullName depends on firstName and lastName, because these get called when evaluating fullName.
        return this.firstName() + " " + this.lastName();
    }, this);
};

becomes:

(defviewmodel data
  (observing :first_name "Bert")
  (observing :last_name  "Bertington")
  (computing :name [:first_name :last_name]
    (str :first_name " " :last_name)))

However, for something harder like:

var BetterListModel = function () {
    this.itemToAdd = ko.observable("");
    this.allItems = ko.observableArray(["Fries", "Eggs Benedict", "Ham", "Cheese"]); // Initial items
    this.selectedItems = ko.observableArray(["Ham"]);                                // Initial selection

    this.addItem = function () {
        if ((this.itemToAdd() != "") && (this.allItems.indexOf(this.itemToAdd()) < 0)) // Prevent blanks and duplicates
            this.allItems.push(this.itemToAdd());
        this.itemToAdd(""); // Clear the text box
    };

    this.removeSelected = function () {
        this.allItems.removeAll(this.selectedItems());
        this.selectedItems([]); // Clear selection
    };

    this.sortItems = function() {
        this.allItems.sort();
    };
};

ko.applyBindings(new BetterListModel());

I'm not sure what I can do in clojurescript to match code like this: this.allItems.push(this.itemToAdd())

Any thoughts?

like image 450
zcaudate Avatar asked Jun 04 '12 06:06

zcaudate


2 Answers

If you need an explicit reference to JavaScript's this dynamic binding, ClojureScript provides a this-as macro:

https://github.com/clojure/clojurescript/blob/master/src/clj/cljs/core.clj#L324

like image 43
Kevin L. Avatar answered Oct 12 '22 10:10

Kevin L.


After lots of trial and error, I figured out how to have the same structure for clojurescript as for javascript.

The this-as macro has a few idiosyncrasies and only works when the method is put into the class

for example I want to create something that looks like this in javascript:

var anobj = {a: 9,
             get_a: function(){return this.a;}};

I have to do a whole lot more coding to get the same object in clojurescript:

(def anobj (js-obj))
(def get_a (fn [] (this-as me (.-a me))))
(aset anobj "a" 9)
(aset anobj "get_a" get_a)

which is seriously ugly for a language as beautiful as clojure. Things get worse when you have got functions that link to each other, like what happens in knockout.

I found that the best way to create an js-object with lots of this's in there is to define a __init__ method, add it to the class and then run it, then remove it from the class. For example, if I wanted to make another object:

var avobj = {a: this,
             b: 98,
             c: this.a
             get_a: function(){return str(this.a) + str(this.c);}};

written as clojurescript with and __init__ method looks like this:

(def avobj (js-obj))
(def av__init__ 
     #(this-as this 
        (aset this "a" this) 
        (aset this "b" 9) 
        (aset this "c" (.-a this))
        (aset this "get_a" (fn [] (str (.-a this) (.-c this))))))
(aset avobj "__init__" av__init__)
(. avobj __init__)
(js-delete stuff "__init__")

There's still a whole bunch more code than javascript... but the most important thing is that you get the same object as javascript. Setting all the variables using this form also allows the use of macros to simplify. So now I have defined a macro:

(defmacro defvar [name & body]
   (list 'do
    (list 'def name
      (list 'map->js 
        {
          :__init__ 
          (list 'fn []
              (list 'this-as 'this
                 (list 'aset 'this "a" "blah")))          
        }))
    ;(. js/console log ~name)
    (list '. name '__init__)
    (list 'js-delete name "__init__")))

and with map->js taken from jayq.utils:

(defn map->js [m]
  (let [out (js-obj)]
    (doseq [[k v] m]
      (aset out (name k) v))
    out))

Now I can write code like this:

(defvar avobj
   a this 
   b 9 
   c (.-a this)
   get_a (fn [] (str (.-a this) (.-c this))))

and for the answer to knockout:

(defvar name_model
    first_name (observable "My")
    last_name (observable "Name")
    name (computed (fn [] (str (. this first_name) " " (. this last_name)))))

(. js/ko (applyBindings name_model));

Which is really nice for me as it matches javascript really well and its entirely readable!

like image 138
2 revs Avatar answered Oct 12 '22 10:10

2 revs