Skip to content

Instantly share code, notes, and snippets.

@lynaghk
Last active July 5, 2022 13:33
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save lynaghk/3856153 to your computer and use it in GitHub Desktop.
Save lynaghk/3856153 to your computer and use it in GitHub Desktop.
Angular.js from ClojureScript
(ns together.todo
(:use-macros [c2.util :only [p pp]])
(:use [clojure.string :only [blank?]]))
(defn oset!
[obj & kvs]
(doseq [[k v] (partition 2 kvs)]
(aset obj (name k) v)))
(def todomvc
"Create a TodoMVC module within Angular.js"
(.module js/angular "todomvc" (array)))
;;Factory fn that provides a map of fns to get/set localStorage.
;;This is just copied from the Angular demo; how useful is the factory pattern in Clojure?
(.factory todomvc "todoStorage"
(fn []
(let [storage-id "todos-angular-cljs"]
(js-obj "get" #(.parse js/JSON (or (.getItem js/localStorage storage-id) "[]"))
"set" (fn [todos]
(.setItem js/localStorage storage-id (.stringify js/JSON todos)))))))
(defn TodoCtrl [$scope $location todoStorage]
;;Angular's $scope.$watch fn takes an expression to watch (here it's 'todos', a property of the $scope) and runs the listener fn whenever the expression's value changes.
;;This happens via dirty-checking rather than some kind of callback mechanism; see:
;;
;; http://stackoverflow.com/questions/9682092/databinding-in-angularjs
;; http://docs.angularjs.org/guide/concepts#runtime
;;
;;for details. Based on the callback nonsense I've run into in using my own reflex library, I'm convinced this is a superior approach.
;;$scope.$watch uses angular.equals for comparison; if we wanted to use cljs data structures here, we'd have to either:
;;1) monkeypatch angular.equals to use the cljs equality fn for efficency
;;2) write a macro around $scope.$watch instead of calling it directly that gives (hash x) to angular instead of x.
(.$watch $scope "todos"
(fn [todos]
(.set todoStorage todos)
(p todos)
(oset! $scope
:doneCount (count (filter #(.-completed %) todos))
:remainingCount (count (remove #(.-completed %) todos))
:allChecked (every? #(.-completed %) todos)))
true)
(.$watch $scope "location.path()"
(fn [path]
(oset! $scope :statusFilter
(case path
"/active" #(not (.-completed %))
"/completed" #(.-completed %)
nil)))
true)
(when (blank? (.path $location))
(.path $location "/"))
(oset! $scope
:todos (.get todoStorage)
:location $location
:addTodo #(when-not (blank? (.-newTodo $scope))
(.push (.-todos $scope) (js-obj "title" (.-newTodo $scope)
"completed" false))
(oset! $scope :newTodo ""))
:editTodo (fn [todo] (oset! $scope :editedTodo todo))
:removeTodo (fn [todo] (.splice (.-todos $scope) (.indexOf (.-todos $scope) todo) 1))
:doneEditing (fn [todo]
(oset! $scope :editedTodo nil)
(when (blank? (.-title todo))
(.removeTodo $scope todo)))
:clearDoneTodos (fn []
(oset! $scope :todos
(.filter (.-todos $scope) #(not (.-completed %)))))
:markAll (fn [done]
(.forEach (.-todos $scope)
#(oset! % :completed done)))))
(.controller todomvc "TodoCtrl" TodoCtrl)
;;Directive that executes an expression when the element it is applied to loses focus
(.directive todomvc "todoBlur"
(fn []
(fn [scope el attrs]
(.bind el "blur"
#(.$apply scope (.-todoBlur attrs))))))
;;Directive that places focus on the element it is applied to when the expression it binds to evaluates to true.
(.directive todomvc "todoFocus"
(fn [$timeout]
(fn [scope el attrs]
(.$watch scope (.-todoFocus attrs)
(fn [new-val]
(when new-val
($timeout #(.focus (aget el 0))
0 false)))))))

Angular.js from ClojureScript

I ran into Miško Hevery at Strange Loop and he got me interested in his Angular.js framework. This gist is an exploration of using Angular from ClojureScript. I've transcribed the Angular.js TodoMVC implementation into ClojureScript, below. For reference, here's a TodoMVC using C2+Reflex.

Conceptually C2 and Angular have the same philosophy re: data-binding, but the implementations are very different. C2 merges Hiccup into a live DOM, so there's no special data binding syntax;

  • Pro: it's all Clojure
  • Con: DOM walking is inefficient; it's all Clojure (one of my designers hates this)

Angular.js has its own lil' data-binding language, but all of that is embedded in strings in HTML markup. Also, life cycles are much more well thought out in Angular vs. my C2+Reflex libraries. Angular uses dirty checking and throws an exception if you get yourself into a potential infinite loop; Reflex just adds callbacks to everything, which is hell to debug infinite loops and also less efficient w.r.t browser redraws. Moar info:

CLJS next steps

I'm keen to ditch my own Reflex and Singult libraries and use Angular.js for future large clientside projects. Angular has much wider adoption, browser compatibility, and code test coverage. The data binding is more efficient and the general design is far more thought out (my interest is data visualization, not clientside frameworks, after all...).

That said, there's no way in hell I'm writing raw JavaScript again, so I want to figure out the ClojureScript/Angular.js story. Many of Angular's directives are written against JavaScript objects, e.g.

    <li ng-repeat="todo in todos | filter:statusFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
      <div class="view">
        <input class="toggle" type="checkbox" ng-model="todo.completed">
        <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
        <button class="destroy" ng-click="removeTodo(todo)"></button>
      </div>
      ...
    </li>

loops over a filtered view of todos, creating <li> elements with certain classes if certain properties or expressions evaluate truthy. One option for ClojureScript would be to write an ng-cljs-repeat attribute that walks an ISeq. However, one of the great things about Angular is that data-binding is two way. E.g., the

    <input class="toggle" type="checkbox" ng-model="todo.completed">

above directly sets the completed property of an individual todo whenever it is checked or unchecked. That is, Angular is relying on the mutable nature of JavaScript objects.

It's not immediately clear to me how we can get Angular's view -> model data binding to work with Clojure's immutable object semantics.

Anyway, something to chew on for a bit. Unlimited whiskeys at the Conj for whomever finds a clever solution that doesn't involve porting all of Angular.js to Clojure = )

--kevin

<!doctype html>
<html lang="en" ng-app="todomvc">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>AngularJS - TodoMVC</title>
<link rel="stylesheet" href="assets/base.css">
<style>[ng-cloak] {display: none}</style>
</head>
<body>
<section id="todoapp" ng-controller="TodoCtrl">
<header id="header">
<h1>todos</h1>
<form id="todo-form" ng-submit="addTodo()">
<input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" autofocus>
</form>
</header>
<section id="main" ng-show="todos.length" ng-cloak>
<input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li ng-repeat="todo in todos | filter:statusFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
<form ng-submit="doneEditing(todo)">
<input class="edit" ng-model="todo.title" todo-blur="doneEditing(todo)" todo-focus="todo == editedTodo">
</form>
</li>
</ul>
</section>
<footer id="footer" ng-show="todos.length" ng-cloak>
<span id="todo-count">
<strong>{{remainingCount}}</strong>
<ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
</span>
<ul id="filters">
<li>
<a ng-class="{selected: location.path() == '/'} " href="#/">All</a>
</li>
<li>
<a ng-class="{selected: location.path() == '/active'}" href="#/active">Active</a>
</li>
<li>
<a ng-class="{selected: location.path() == '/completed'}" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" ng-click="clearDoneTodos()" ng-show="doneCount">Clear completed ({{doneCount}})</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo.</p>
<p>Credits:
<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
<a href="http://ericbidelman.com">Eric Bidelman</a>,
<a href="http://jacobmumm.com">Jacob Mumm</a> and
<a href="http://igorminar.com">Igor Minar</a>
</p>
</footer>
<script src="angular.js"></script>
<script src="together.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment