When I wrote my last post on ClojureScript, I was really hoping someone would jump in and say: “You’re doing it wrong! Here’s how.”
I did get some interesting replies, especially on HackerNews (where that post was briefly on the front page). There really seem to be two camps here: Newbies as confused as I am, and pros who say you just have to invest the time and learn, then you may be able to make good use of some of existing JS frameworks or (better?) roll your own ClojureScript frameworks. They say it’s worth it once your codebase is big enough.
Getting Angular to Work
Anyway, Greg Weber here on my blog noted that you can actually use Angular with Closure – just need to use explicit dependency injection. So far Angular seemed to require the least work with CLJS, so I was happy to give it another shot. I also found this note on minification in Angular docs very helpful.
In the end I’ve successfully rewritten the “todo” sample application. Here’s one way to do it:
(defn add-todo [scope]
(fn []
(.push (.-todos scope) (js-obj "text" (.-todoText scope) "done" false))
(aset scope "todoText" "")))
(defn remaining [scope]
(fn []
(count (filter #(not (.-done %)) (.-todos scope)))))
(defn archive [scope]
(fn []
(let [arr (into-array (filter #(not (.-done %)) (.-todos scope)))]
(aset scope "todos" arr ))))
(defn CTodoCtrl [$scope]
(def $scope.todos (array (js-obj "text" "learn angular" "done" true)))
(def $scope.addTodo (add-todo $scope))
(def $scope.remaining (remaining $scope))
(def $scope.archive (archive $scope)))
(def TodoCtrl
(array
"$scope"
CTodoCtrl))
The last 4 lines are equivalent of using this array syntax in JavaScript:
TodoCtrl = ['$scope', CTodoCtrl];
Another way to do it is setting the $inject
property, like this:
(def TodoCtrl CTodoCtrl)
(aset TodoCtrl "$inject" (array "$scope"))
As usually, complete working project can be found at my GitHub repository.
Implementation Details
Function definition
In the above example I’m defining functions on CTodoCtrl
by using “factory functions”. I find this slightly more readable, but it also can be done with in-place definitions like this:
(aset $scope "remaining"
(fn []
(count (filter #(not (.-done %)) (.-todos $scope)))))
Unfortunately, I was unable to get it to work with anonymous functions (it compiled to CTodoCtrl.remaining = (function CTodoCtrl.remaining() {...
):
(aset $scope "remaining" #(...))
This did not work either (I wish it did!):
(defn $scope.remaining [] (...))
Objects, Arrays
I’m not quite happy with the use of objects here – I would definitely prefer to use Clojure maps like this:
; Instead of:
; (def $scope.todos (array (js-obj "text" "learn angular" "done" true)))
; Do:
(def $scope.todos [{:text "learn angular" :done true}])
; Insetad of:
; (into-array (filter #(not (.-done %)) (.-todos scope)))
; Do:
(filter #(not (:done %)) (:todos scope))
Unfortunately, it seems Angular doesn’t like ClojureScript types and vice versa. Looks like a small, fixable annoyance.
ClojureScript!
It’s still ugly at places and not quite spectacular, but I like using functional programming with ClojureScript instead of JavaScript loops.
I mean replacing this:
var count = 0;
angular.forEach($scope.todos, function(todo) {
count += todo.done ? 0 : 1;
});
return count;
with:
(count (filter #(not (.-done %)) (.-todos scope)))
And this:
var oldTodos = $scope.todos;
$scope.todos = [];
angular.forEach(oldTodos, function(todo) {
if (!todo.done) $scope.todos.push(todo);
});
with:
(let [arr (into-array (filter #(not (.-done %)) (.-todos scope)))]
(aset scope "todos" arr))
Verdict
All in all, I may finally be seeing the light at the end of the tunnel. Integration with Angular looks very promising, after addressing the small interop glitches with type mapping it may be quite expressive and straightforward. I probably will shelve Knockout for now and explore Angular.