├── repl ├── doc └── intro.md ├── .gitignore ├── repl.clj ├── test └── bbloom │ ├── vdom_test.clj │ └── vdom │ ├── core_test.clj │ └── patch_test.clj ├── index.html ├── project.clj ├── src └── bbloom │ └── vdom │ ├── util.cljc │ ├── playground.cljs │ ├── trace.cljc │ ├── syntax.cljc │ ├── browser.cljs │ ├── patch.cljc │ └── core.cljc ├── README.md └── LICENSE /repl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rlwrap lein run -m clojure.main repl.clj 4 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to bbloom.vdom 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.repl* 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /repl.clj: -------------------------------------------------------------------------------- 1 | (load-file "build.clj") 2 | 3 | (require 'cljs.repl) 4 | (require 'cljs.repl.browser) 5 | 6 | (cljs.repl/repl (cljs.repl.browser/repl-env) 7 | :watch "src" 8 | :output-dir "target/out") 9 | -------------------------------------------------------------------------------- /test/bbloom/vdom_test.clj: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom-test 2 | (:require [clojure.test :refer :all] 3 | [bbloom.vdom :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/bbloom/vdom/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.core-test 2 | (:use bbloom.vdom.core)) 3 | 4 | (-> null 5 | (create-element :x "div") 6 | (set-props :x {"a" 1}) 7 | (set-props :x {"b" 2}) 8 | ;(set-props :x {"a" nil}) 9 | (set-props :x {"nest" {"x" 3}}) 10 | ;(set-props :x {"nest" {"x" nil}}) 11 | fipp.edn/pprint 12 | ) 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject bbloom.vdom "0.0.2" 2 | :description "A ClojureScript virtual-dom library." 3 | :url "https://github.com/brandonbloom/cljs-vdom" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0"] 7 | [org.clojure/clojurescript "0.0-3308"] 8 | [org.clojure/core.rrb-vector "0.0.11"]]) 9 | -------------------------------------------------------------------------------- /src/bbloom/vdom/util.cljc: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.util 2 | (:require [clojure.core.rrb-vector :as rrb])) 3 | 4 | (defn index-of [v x] 5 | (let [n (count v)] 6 | (loop [i 0] 7 | (cond 8 | (= i n) nil 9 | (= (nth v i) x) i 10 | :else (recur (inc i)))))) 11 | 12 | (defn remove-at [v i] 13 | (rrb/catvec (rrb/subvec v 0 i) (rrb/subvec v (inc i)))) 14 | 15 | (defn remove-item [v x] 16 | (remove-at v (index-of v x))) 17 | 18 | (defn insert [v i x] 19 | (rrb/catvec (rrb/subvec v 0 i) [x] (rrb/subvec v i))) 20 | -------------------------------------------------------------------------------- /src/bbloom/vdom/playground.cljs: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.playground 2 | (:require [cljs.pprint :refer [pprint]] 3 | [clojure.browser.repl :as repl] 4 | [bbloom.vdom.core :as vdom] 5 | [bbloom.vdom.browser :refer [render]] 6 | [bbloom.vdom.syntax :refer [seqs->vdom]] ;XXX 7 | )) 8 | 9 | (defonce conn 10 | (repl/connect "http://localhost:9000/repl")) 11 | 12 | (enable-console-print!) 13 | 14 | (println "Hello world!") 15 | 16 | ;; defonce because functions have reference equality. 17 | (defonce onclick 18 | (fn [e] 19 | (.log js/console "onclick" e))) 20 | 21 | (def tree 22 | `(div {"tabindex" 0} 23 | (i {:key "c"} "italic!") 24 | (span {:key "a"} "foo") 25 | (b {} #_(i {} "bar")) 26 | (div {:key "b" 27 | "style" {"color" "red"}} "foox") 28 | (input {:key "c" 29 | "value" "abx"}) 30 | (button {"onclick" ~onclick} 31 | "click me") 32 | )) 33 | 34 | (-> tree 35 | seqs->vdom 36 | (vdom/mount "root" [["div" 0]]) 37 | render 38 | (select-keys [#_:trace :created :destroyed]) 39 | pprint) 40 | -------------------------------------------------------------------------------- /test/bbloom/vdom/patch_test.clj: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.patch-test 2 | (:require [bbloom.vdom.core :as vdom] 3 | [bbloom.vdom.syntax :refer [seqs->vdom]] 4 | [bbloom.vdom.trace :refer [traced]]) 5 | (:use bbloom.vdom.patch)) 6 | 7 | (defn assert-patch [before after] 8 | (let [after* (patch before after)] 9 | (fipp.edn/pprint 10 | (if (not= after* after) 11 | {:before before 12 | :expected after 13 | :actual after*} 14 | {:before before 15 | :after after})))) 16 | 17 | (defn party [before after] 18 | (assert-patch before after) 19 | (fipp.edn/pprint (diff before after)) 20 | ) 21 | 22 | (comment 23 | 24 | (let [vdom (seqs->vdom '(div {"tabindex" 0} 25 | (span {:key "k"} "foo") 26 | (b {} "bar"))) 27 | ;vdom (vdom/mount vdom "blah" [["div" 0]]) 28 | ] 29 | ;(party vdom/null vdom) 30 | (party vdom vdom/null) 31 | ) 32 | 33 | (let [vdom (-> vdom/null 34 | (vdom/create-element :x "div") 35 | (vdom/set-props :x {"tabindex" 1 "style" {"color" "red"}}))] 36 | ;(party vdom (vdom/set-props vdom :x {"tabindex" 2})) 37 | (party vdom (vdom/set-props vdom :x {"style" {"background" "green"}})) 38 | ) 39 | 40 | ) 41 | -------------------------------------------------------------------------------- /src/bbloom/vdom/trace.cljc: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.trace 2 | (:require [bbloom.vdom.core :as vdom])) 3 | 4 | ;; The redundancy in this file is mildly annoying, but 5 | ;; my naive attempt to macro-ize it was majorly ugly. 6 | 7 | (defrecord DomTrace [trace dom] 8 | 9 | vdom/IDom 10 | 11 | ;; Accessors (passthrough to underlying vdom). 12 | (node [vdom id] (vdom/node dom id)) 13 | (nodes [vdom] (vdom/nodes dom)) 14 | (mounts [vdom] (vdom/mounts dom)) 15 | (hosts [vdom] (vdom/hosts dom)) 16 | 17 | ;; Manipulations (add [vdom op] to trace before passthrough). 18 | 19 | (mount [vdom eid id] 20 | (DomTrace. (conj trace [dom [:mount eid id]]) 21 | (vdom/mount dom eid id))) 22 | 23 | (unmount [vdom id] 24 | (DomTrace. (conj trace [dom [:unmount id]]) 25 | (vdom/unmount dom id))) 26 | 27 | (detach [vdom id] 28 | (DomTrace. (conj trace [dom [:detach id]]) 29 | (vdom/detach dom id))) 30 | 31 | (create-text [vdom id text] 32 | (DomTrace. (conj trace [dom [:create-text id text]]) 33 | (vdom/create-text dom id text))) 34 | 35 | (set-text [vdom id text] 36 | (DomTrace. (conj trace [dom [:set-text id text]]) 37 | (vdom/set-text dom id text))) 38 | 39 | (create-element [vdom id tag] 40 | (DomTrace. (conj trace [dom [:create-element id tag]]) 41 | (vdom/create-element dom id tag))) 42 | 43 | (set-props [vdom id props] 44 | (DomTrace. (conj trace [dom [:set-props id props]]) 45 | (vdom/set-props dom id props))) 46 | 47 | (insert-child [vdom parent-id index child-id] 48 | (DomTrace. (conj trace [dom [:insert-child parent-id index child-id]]) 49 | (vdom/insert-child dom parent-id index child-id))) 50 | 51 | (free [vdom id] 52 | (DomTrace. (conj trace [dom [:free id]]) 53 | (vdom/free dom id))) 54 | 55 | ) 56 | 57 | (defn traced [vdom] 58 | (DomTrace. [] vdom)) 59 | -------------------------------------------------------------------------------- /src/bbloom/vdom/syntax.cljc: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.syntax 2 | "Parse an Edn tree in to a vdom graph." 3 | (:require [bbloom.vdom.core :as vdom])) 4 | 5 | ;;XXX The code in this file is only really used for debugging and 6 | ;;XXX should not be part of the public API. Move to test directory? 7 | 8 | ;;XXX Should this use vdom/create-element, etc rather than direct updates? 9 | ;;^^^ Nah, instead validate the result. 10 | 11 | (declare seqs->maps) 12 | 13 | (defn seq->map [[tag props & children]] 14 | {:tag (name tag) ;XXX ignores namespace (on purpose for syntax quote) 15 | :props props 16 | :children (mapv seqs->maps children)}) 17 | 18 | (defn seqs->maps [x] 19 | (cond 20 | (string? x) {:tag :text :text x} 21 | (seq? x) (seq->map x) 22 | :else (throw (ex-info "Invalid dom syntax" {:val x})))) 23 | 24 | (defn assign-ids 25 | "Simple ID scheme of visual-tree paths. Each path element is a pair of 26 | the node type and either the index within the parent or an explicit key." 27 | ([x] 28 | (first (assign-ids [x] []))) 29 | ([xs path] 30 | (mapv (fn [i x] 31 | (let [tag (:tag x) 32 | k (get-in x [:props :key] i) 33 | id (conj path [tag k]) 34 | x (assoc x :id id)] 35 | (if (string? tag) 36 | (-> x 37 | (update :props dissoc :key) 38 | (update :children assign-ids id)) 39 | x))) 40 | (range) 41 | xs))) 42 | 43 | (defn maps->nodes 44 | ([x] (maps->nodes {} x)) 45 | ([nodes {:keys [id children] :as x}] 46 | (assert (some? id) (str "No id for node: " x)) 47 | (assert (nil? (nodes id)) (str "Duplicate ID: " id)) 48 | (reduce maps->nodes 49 | (assoc nodes id 50 | (if (-> x :tag string?) 51 | (update x :children #(mapv :id %)) 52 | x)) 53 | (map #(assoc % :parent id) children)))) 54 | 55 | (defn maps->vdom [x] 56 | (let [g (maps->nodes x)] 57 | (assoc vdom/null :nodes g :detached #{(:id x)}))) 58 | 59 | (defn seqs->vdom [x] 60 | (-> x seqs->maps assign-ids maps->vdom)) 61 | 62 | (comment 63 | 64 | (require '[bbloom.vdom.core :as vdom]) 65 | 66 | (-> '(div {"tabindex" 0} 67 | (span {:key "k"} "foo") 68 | (b {} "bar")) 69 | seqs->maps 70 | assign-ids 71 | maps->vdom 72 | (vdom/mount "root" [["div" 0]]) 73 | ;(mount "root" [0 "k"]) 74 | fipp.edn/pprint 75 | ) 76 | 77 | ) 78 | -------------------------------------------------------------------------------- /src/bbloom/vdom/browser.cljs: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.browser 2 | (:require [bbloom.vdom.core :as vdom] 3 | [bbloom.vdom.patch :refer [trace-patch]])) 4 | 5 | (defonce global (atom {:vdom vdom/null :node->id {} :id->node {}})) 6 | 7 | (defmulti mutate (fn [state [method & args]] method)) 8 | 9 | (defn render [vdom] 10 | (let [state @global 11 | trace (trace-patch (:vdom state) vdom) 12 | state (reduce (fn [state [vdom op]] 13 | (mutate (assoc state :vdom vdom) op)) 14 | (assoc state :vdom vdom :created [] :destroyed []) 15 | trace)] 16 | (reset! global (dissoc state :created :destroyed)) 17 | (-> state 18 | (select-keys [:created :destroyed]) 19 | (assoc :trace trace)))) 20 | 21 | (defn lookup [id] 22 | (get-in @global [:id->node id])) 23 | 24 | (defn identify [node] 25 | (get-in @global [:node->id node])) 26 | 27 | (defmethod mutate :mount [{:keys [id->node] :as state} [_ eid id]] 28 | (let [el (.getElementById js/document eid)] 29 | (assert el (str "No element with id: " eid)) 30 | (.appendChild el (id->node id)) 31 | state)) 32 | 33 | (defn- detach [{:keys [id->node] :as state} id] 34 | (let [child (id->node id)] 35 | (.removeChild (.-parentNode child) child)) 36 | state) 37 | 38 | (defmethod mutate :unmount [state [_ id]] 39 | (detach state id)) 40 | 41 | (defmethod mutate :detach [state [_ id]] 42 | (detach state id)) 43 | 44 | (defn create [state id node] 45 | (-> state 46 | (update :created conj [id node]) 47 | (assoc-in [:id->node id] node) 48 | (assoc-in [:node->id node] id))) 49 | 50 | (defmethod mutate :create-text [state [_ id text]] 51 | (create state id (.createTextNode js/document text))) 52 | 53 | (defmethod mutate :set-text [{:keys [id->node] :as state} [_ id text]] 54 | (set! (.-nodeValue (id->node id)) text) 55 | state) 56 | 57 | (defmethod mutate :create-element [state [_ id tag]] 58 | (create state id (.createElement js/document tag))) 59 | 60 | (defmethod mutate :set-props [{:keys [id->node] :as state} [_ id props]] 61 | (let [node (id->node id)] 62 | (doseq [[k v] (dissoc props "attributes" "style")] 63 | ;;XXX for nil values, virtual-dom sets to "" if prev was a string. Why? 64 | ;;^^^ If this is necessary, can it be done during diff? 65 | (aset node k v)) 66 | (doseq [[k v] (props "attributes")] 67 | (if (nil? v) 68 | (.removeAttribute node k) 69 | (.setAttribute node k v))) 70 | (doseq [[k v] (props "style")] 71 | (aset node "style" k (if (nil? v) "" v)))) 72 | state) 73 | 74 | (defmethod mutate :insert-child 75 | [{:keys [id->node] :as state} [_ parent-id index child-id]] 76 | (let [parent (id->node parent-id) 77 | siblings (.-children parent) 78 | child (id->node child-id)] 79 | (if (= (alength siblings) index) 80 | (.appendChild parent child) 81 | (let [sibling (aget siblings index)] 82 | (.insertBefore parent child sibling))) 83 | state)) 84 | 85 | (defmethod mutate :free [{:keys [vdom id->node] :as state} [_ id]] 86 | ((fn rec [state id] 87 | (let [node (id->node id)] 88 | (reduce rec 89 | (-> state 90 | (update :destroyed conj [id node]) 91 | (update-in [:id->node] dissoc id) 92 | (update-in [:node->id] dissoc node)) 93 | (:children (vdom/node vdom id))))) 94 | state id)) 95 | -------------------------------------------------------------------------------- /src/bbloom/vdom/patch.cljc: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.patch 2 | (:refer-clojure :exclude [create-node]) ;XXX core leaking this private? 3 | (:require [clojure.set :as set] 4 | [bbloom.vdom.core :as vdom] 5 | [bbloom.vdom.trace :refer [traced]])) 6 | 7 | (defn update-text [vdom before {:keys [id text] :as after}] 8 | (if (= (:text before) text) 9 | vdom 10 | (vdom/set-text vdom id text))) 11 | 12 | (defn detach-last-child [vdom id] 13 | (vdom/detach vdom (-> (vdom/node vdom id) :children peek))) 14 | 15 | (defn diff-maps [before after] 16 | (let [removed (set/difference (-> before keys set) (-> after keys set))] 17 | (reduce (fn [acc [k val]] 18 | (let [old (get before k)] 19 | (cond 20 | (= old val) acc 21 | (map? val) (let [sub (diff-maps old val)] 22 | (if (seq sub) 23 | (assoc acc k sub) 24 | acc)) 25 | :else (assoc acc k val)))) 26 | (when (seq removed) 27 | (into {} (map #(vector % nil)) removed)) 28 | after))) 29 | 30 | (defn update-element [vdom before {:keys [id props] :as after}] 31 | (let [updated (diff-maps (:props before) props)] 32 | (if (seq updated) 33 | (vdom/set-props vdom id updated) 34 | vdom))) 35 | 36 | (defn update-node [vdom before {:keys [id tag] :as after}] 37 | (assert (= (:tag before) tag) (str "Cannot transmute node type for id " id)) 38 | (if (= tag :text) 39 | (update-text vdom before after) 40 | (update-element vdom before after))) 41 | 42 | (defn create-node [vdom {:keys [id tag] :as node}] 43 | (if (= tag :text) 44 | (vdom/create-text vdom id (:text node)) 45 | (let [{:keys [props]} node 46 | vdom (vdom/create-element vdom id tag)] 47 | (if (seq props) 48 | (vdom/set-props vdom id (:props node)) 49 | vdom)))) 50 | 51 | (defn patch-node [vdom {:keys [id tag] :as node}] 52 | (if-let [before (vdom/node vdom id)] 53 | (update-node vdom before node) 54 | (create-node vdom node))) 55 | 56 | (def ^:dynamic *parented*) ;XXX debug-only 57 | 58 | (defn patch-children [vdom {:keys [id children]}] 59 | (let [;; Move desired children in to place. 60 | vdom (transduce 61 | (map-indexed vector) 62 | (completing 63 | (fn [vdom [i child]] 64 | (assert (nil? (*parented* child)) 65 | (str "Duplicate node id: " child)) 66 | (set! *parented* (conj *parented* child)) 67 | (if (= (get-in (vdom/node vdom id) [:children i]) child) 68 | vdom 69 | (vdom/insert-child vdom id i child)))) 70 | vdom 71 | children) 72 | ;; Detach any leftover trailing children. 73 | n (max 0 (- (count (:children (vdom/node vdom id))) 74 | (count children))) 75 | vdom (nth (iterate (fn [vdom] 76 | (detach-last-child vdom id)) 77 | vdom) 78 | n)] 79 | vdom)) 80 | 81 | (defn patch [vdom goal] 82 | (let [N0 (vdom/nodes vdom), M0 (vdom/mounts vdom), H0 (vdom/hosts vdom) 83 | N1 (vdom/nodes goal), M1 (vdom/mounts goal), H1 (vdom/hosts goal) 84 | ;; Unmount. 85 | vdom (transduce (comp (remove (fn [[eid nid]] (= (M1 eid) nid))) 86 | (map second)) 87 | (completing vdom/unmount) 88 | vdom M0) 89 | ;; Patch. 90 | vdom (reduce patch-node vdom N1) 91 | vdom (binding [*parented* #{}] 92 | (transduce (filter #(-> % :tag string?)) 93 | (completing patch-children) 94 | vdom N1)) 95 | ;; Free. 96 | freed (set/difference (into #{} (map :id) N0) (into #{} (map :id) N1)) 97 | vdom (transduce (remove #(:parent (vdom/node vdom %))) 98 | (completing vdom/free) 99 | vdom freed) 100 | ;; Mount. 101 | vdom (transduce (remove (fn [[nid eid]] (= (H0 nid) eid))) 102 | (completing (fn [vdom [nid eid]] 103 | (vdom/mount vdom eid nid))) 104 | vdom H1)] 105 | vdom)) 106 | 107 | (defn trace-patch [before after] 108 | (-> (traced before) (patch after) :trace)) 109 | 110 | (defn diff [before after] 111 | (map second (trace-patch before after))) 112 | -------------------------------------------------------------------------------- /src/bbloom/vdom/core.cljc: -------------------------------------------------------------------------------- 1 | (ns bbloom.vdom.core 2 | (:require [bbloom.vdom.util :refer [remove-item insert]])) 3 | 4 | (defprotocol IDom 5 | "Models multiple DOM trees relationally (ie. linked by IDs). The root of 6 | each tree can be either 'mounted' on to an external DOM or 'detached'. 7 | Provides a minimal set of atomic manipulations which mirror efficient 8 | mutations of a browser DOM." 9 | ;; Accessors 10 | (node [vdom id]) 11 | (nodes [vdom]) 12 | (mounts [vdom]) 13 | (hosts [vdom]) 14 | ;; Manipulations 15 | (mount [vdom eid id]) 16 | (unmount [vdom id]) 17 | (detach [vdom id]) 18 | (create-text [vdom id text]) 19 | (set-text [vdom id text]) 20 | (create-element [vdom id tag]) 21 | (set-props [vdom id props] 22 | "Merges props to the node, removing those set to nil. 23 | Map values are treated recursively.") 24 | (insert-child [vdom parent-id index child-id]) 25 | (free [vdom id]) 26 | ) 27 | 28 | ;;TODO :parent is stored on nodes directly, should it be a top-level map? 29 | (defrecord VDom [nodes mounts hosts detached] 30 | 31 | IDom 32 | 33 | ;; Accessors 34 | 35 | (node [vdom id] 36 | (get-in vdom [:nodes id])) 37 | 38 | (nodes [vdom] 39 | (-> vdom :nodes vals)) 40 | 41 | (mounts [vdom] 42 | (:mounts vdom)) 43 | 44 | (hosts [vdom] 45 | (:hosts vdom)) 46 | 47 | ;; Manipulations 48 | 49 | (mount [vdom eid id] 50 | (let [n (get-in vdom [:nodes id])] 51 | (assert n (str "Cannot mount unknown node: " id)) 52 | (assert (nil? (:parent n)) (str "Cannot mount interior node: " id))) 53 | (assert (nil? (get-in vdom [:hosts id])) (str "Already mounted: " id)) 54 | (-> vdom 55 | (assoc-in [:mounts eid] id) 56 | (assoc-in [:hosts id] eid) 57 | (update :detached disj id))) 58 | 59 | (unmount [vdom id] 60 | (let [n (get-in vdom [:nodes id])] 61 | (assert n (str "Cannot unmount unknown node: " id)) 62 | (assert (get-in vdom [:hosts id]) (str "Node already not mounted: " id)) 63 | (-> vdom 64 | (update :mounts dissoc id) 65 | (update :hosts dissoc id) 66 | (update :detached conj id)))) 67 | 68 | (detach [vdom id] 69 | (let [{:keys [parent] :as n} (get-in vdom [:nodes id])] 70 | (assert n (str "No such node id: " id)) 71 | (assert parent (str "No already detached: " id)) 72 | (-> vdom 73 | (update-in [:nodes parent :children] remove-item id) 74 | (update-in [:nodes id] dissoc :parent) 75 | (update :detached conj id)))) 76 | 77 | (create-text [vdom id text] 78 | (assert (nil? (get-in vdom [:nodes id])) (str "Node already exists: " id)) 79 | (-> vdom 80 | (assoc-in [:nodes id] {:id id :tag :text :text text}) 81 | (update :detached conj id))) 82 | 83 | (set-text [vdom id text] 84 | (assert (= (get-in vdom [:nodes id :tag]) :text) 85 | (str "Cannot set text of non-text node: " id)) 86 | (assoc-in vdom [:nodes id :text] text)) 87 | 88 | (create-element [vdom id tag] 89 | (assert (nil? (get-in vdom [:nodes id])) (str "Node already exists: " id)) 90 | (-> vdom 91 | (assoc-in [:nodes id] {:id id :tag tag :children []}) 92 | (update :detached conj id))) 93 | 94 | (set-props [vdom id props] 95 | (assert (string? (get-in vdom [:nodes id :tag])) 96 | (str "Cannot set props of non-element node: " id)) 97 | (update-in vdom [:nodes id :props] 98 | (fn [old-props] 99 | (reduce (fn rec [acc [k v]] 100 | (cond 101 | (nil? v) (dissoc acc k) 102 | (map? v) (assoc acc k (reduce rec (get acc k) v)) 103 | :else (assoc acc k v))) 104 | old-props 105 | props)))) 106 | 107 | (insert-child [vdom parent-id index child-id] 108 | ;;TODO This needs an assert or two. 109 | (let [n (get-in vdom [:nodes child-id]) 110 | vdom (if-let [p (:parent n)] 111 | (update-in vdom [:nodes p :children] remove-item child-id) 112 | vdom)] 113 | (-> vdom 114 | (assoc-in [:nodes child-id :parent] parent-id) 115 | (update-in [:nodes parent-id :children] insert index child-id) 116 | (update-in [:detached] disj child-id)))) 117 | 118 | (free [vdom id] 119 | (assert (get-in vdom [:detached id]) 120 | (str "Cannot free non-detached node:" id)) 121 | ((fn rec [vdom id] 122 | (reduce rec 123 | (update vdom :nodes dissoc id) 124 | (get-in vdom [:nodes id :children]))) 125 | (update vdom :detached disj id) id)) 126 | 127 | ) 128 | 129 | (def null (map->VDom {:nodes {} :mounts {} :hosts {} :detached #{}})) 130 | 131 | (defn valid? [vdom] 132 | ;;XXX validate vdom 133 | true) 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bbloom.vdom 2 | 3 | Yet another Virtual DOM library (in ClojureScript). 4 | 5 | ## Usage 6 | 7 | Don't. At least not yet. 8 | 9 | But if you want to play with it, checkout [playground.cljs][2]. 10 | 11 | The playground code uses an example "mini-framework" called 12 | [bbloom.vdom.syntax][4]. This mini-framework predates the [IDom protocol][5] 13 | and violates encapsulation. The mini-framework will be rewritten/eliminated 14 | soon. My forthcoming framework relies on IDom, so the protocol is likely to 15 | remain somewhat stable. 16 | 17 | ## Goals 18 | 19 | - Low-level design with minimal policy (like [virtual-dom][3]). 20 | - Enable idiomatic use from ClojureScript. 21 | - Be as fast as necessary for responsive user interfaces. 22 | - Provide a richer abstraction of the DOM. See "novelty" below. 23 | - Offer host escape hatches without callback hooks. 24 | 25 | ## Non-Goals 26 | 27 | Or just goals to be realized by a higher layer. 28 | 29 | - Assign node IDs automatically. 30 | - Normalize browser behavior. 31 | - Provide a widget or component model (a la [React.js][1]). 32 | - Win any benchmarks. 33 | - Be usable from JavaScript. 34 | - Support older browsers. 35 | 36 | ## Novelty 37 | 38 | What makes this Virtual DOM library different? 39 | 40 | - Represent the DOM as an immutable graph value. 41 | - Manage detached nodes. 42 | - Support re-parenting (eg for drag-and-drop). 43 | 44 | ## Motivation 45 | 46 | There's a great many problems that more-feature-complete DOM libraries such 47 | as React.js tackle. A low-level virtual DOM library is not an appropriate 48 | platform for application developers. However, a high-level virtual DOM library 49 | bakes in a large amount of policy that may be inappropriate for alternative 50 | frameworks. For example, React uses runtime techniques to normalize browser 51 | behavior, but given compiler support (such as ClojureScript macros), you may 52 | choose a more static approach to work around browser quirks or optimize 53 | inline styles. 54 | 55 | I wanted a low-level DOM library for experimenting with high-level frameworks 56 | from ClojureScript. My intention is to enforce strict system layering, but 57 | ownership over every layer enables me to make changes in the appropriate 58 | place. 59 | 60 | ## Approach 61 | 62 | Generalized tree diffing has high algorithmic complexity, so virtual DOM 63 | implementations take advantage of the time dimension by diffing trees 64 | level-by-level on the assumption that there are typically few large structural 65 | changes. Worst case becomes linear and early-out tests for subtrees means that 66 | typical diffing is logarithmic. 67 | 68 | An alternative approach to capitalize on the same assumptions is to do the 69 | equivalent of a "hash join" when calculating a diff. If every node has a 70 | unique ID, and those IDs are stable over time, fast linear diffing is easily 71 | achieved by visiting each node in the new virtual DOM and looking it up in the 72 | previous virtual DOM's hash table. 73 | 74 | This approach allows more freedom of movement for nodes around the tree, at the 75 | cost of the additional bookkeeping required to ensure ID stability. A component 76 | model built on this foundation can provide automatic-ID assignment via 77 | structural path + discriminator key, which matches the level-by-level approach. 78 | While the virtual DOM diffing remains linear, the higher-level can easily 79 | recover the typically-logarithmic times, which is where the constant factors 80 | are higher (for data fetching, change detection, etc). However, such a 81 | component model could also represent IDs beyond the scope of a single parent, 82 | and therefore support reparenting of real DOM nodes when virtual nodes are 83 | reparented. This reparenting is useful for complex interactions such as 84 | drag-and-drop between containers, pop-out panels, or components shared between 85 | different views. 86 | 87 | To this end, the virtual DOM is not represented as a tree, but as a graph. 88 | The graph is itself represented as several indexes over structured nodes 89 | which have an ID, type/tag, properties, a parent, and ordered children. The 90 | graph is designed to be "mounted" on to zero or more places in a "host" DOM. 91 | Virtual nodes can reparent between mounts and even exist detached from any 92 | mount. This more closely models the actual DOM APIs, where nodes can move 93 | freely and may have a null parent. Immutability is used pervasively and 94 | reference equality is maintained wherever possible for fast equality checks. 95 | Patches are represented in terms of traditional DOM API manipulations, so that 96 | there's a nearly one-to-one operational interpretation. 97 | 98 | Great as the virtual DOM idea is, there's plenty of other useful code out there 99 | that is worth leveraging. It must remain possible to integrate with the real 100 | browser DOM without compromising the integrety of the virtual DOM abstraction. 101 | Rather than exposing callback-based lifecycle hooks, applying a diff to the 102 | browser DOM maintains the virtual/real mapping and reports created or freed 103 | nodes for processing at the level of a component model. 104 | 105 | ## Contributing 106 | 107 | Ping me if you're interested, I'm excited to discuss what you've got in mind. 108 | 109 | ## License 110 | 111 | Copyright © 2015 Brandon Bloom 112 | 113 | Distributed under the Eclipse Public License either version 1.0 or (at 114 | your option) any later version. 115 | 116 | 117 | [1]: https://facebook.github.io/react/ 118 | [2]: ./src/bbloom/vdom/playground.cljs 119 | [3]: https://github.com/Matt-Esch/virtual-dom 120 | [4]: ./src/bbloom/vdom/syntax.cljc 121 | [5]: ./src/bbloom/vdom/core.cljc 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------