├── deps.edn ├── shadow-cljs.edn ├── package.json ├── src ├── main │ └── shadow │ │ ├── arborist.clj │ │ ├── arborist │ │ ├── protocols.cljs │ │ ├── attributes.cljs │ │ ├── common.cljs │ │ ├── interpreted.cljs │ │ ├── collections.cljs │ │ ├── fragments.cljs │ │ ├── components.cljs │ │ └── fragments.clj │ │ └── arborist.cljs └── test │ └── shadow │ └── arborist_test.clj ├── .gitignore └── README.md /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/main"] 2 | :deps 3 | {org.clojure/clojure {:mvn/version "1.10.0"} 4 | org.clojure/clojurescript {:mvn/version "1.10.520"}}} 5 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths 3 | ["src/dev" 4 | "src/main" 5 | "src/test"] 6 | 7 | :dependencies 8 | [] 9 | 10 | :builds 11 | {}} 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadow-arborist", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "shadow-cljs": "^2.8.37" 7 | }, 8 | "dependencies": { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/shadow/arborist.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist 2 | (:require [shadow.arborist.fragments :as fragments])) 3 | 4 | (defmacro << [& body] 5 | (fragments/make-fragment &env body)) 6 | 7 | (defmacro fragment [& body] 8 | (fragments/make-fragment &env body)) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /tmp 6 | /checkouts 7 | /src/gen 8 | /out 9 | /examples/todomvc/js 10 | 11 | /.classpath 12 | /.project 13 | /.settings 14 | 15 | pom.xml 16 | pom.xml.asc 17 | *.iml 18 | *.jar 19 | *.log 20 | .shadow-cljs 21 | .idea 22 | .lein-* 23 | .nrepl-* 24 | .DS_Store 25 | 26 | .hgignore 27 | .hg/ 28 | -------------------------------------------------------------------------------- /src/test/shadow/arborist_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist-test 2 | (:require 3 | [clojure.test :as t :refer (deftest is)] 4 | [clojure.pprint :refer (pprint)] 5 | [clojure.string :as str] 6 | [shadow.arborist.fragments :as frag])) 7 | 8 | (def test-body 9 | '[[:foo {:i/key :key :bar 1 :foo foo :x nil :bool true}] 10 | "hello" 11 | [:div#id 12 | (dynamic-thing {:x 1}) 13 | (if (even? 1) 14 | (<< [:h1 "even"]) 15 | (<< [:h2 "odd"])) 16 | [:h1 "hello, " title ", " foo] 17 | (let [x 1] 18 | (<< [:h2 x]))] 19 | 1 20 | (some-fn 1 2)]) 21 | 22 | (def test-body 23 | #_'[[:div.card {:style {:color "red"} :foo ["xyz" "foo" "bar"] :attr toggle} 24 | [:div.card-header title] 25 | [:div.card-body {:on-click ^:once [::foo {:bar yo}] :attr "foo"} "Hello"]]] 26 | 27 | '[[:div.card 28 | [:div.card-title title] 29 | [:div.card-body body] 30 | [:div.card-actions 31 | [:button "ok"]]]] 32 | #_[[:div x] 33 | [:> component {:foo "bar"} [:c1 [:c2 {:x x}] y] [:c3]]]) 34 | 35 | 36 | (deftest test-macro-expand 37 | (pprint (frag/make-fragment {} test-body))) 38 | -------------------------------------------------------------------------------- /src/main/shadow/arborist/protocols.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.protocols) 2 | 3 | (defprotocol IControlFragment 4 | (fragment-build [this env vals] "returns fragment state, required for update") 5 | (fragment-update [this env root nodes ovals nvals])) 6 | 7 | (defprotocol IConstruct 8 | (as-managed [this env])) 9 | 10 | (defprotocol ITreeNode 11 | ;; perform all required node updates, call sync! on all managed nodes 12 | ;; FIXME: calling sync! recursively is bad as stack may get deep 13 | ;; should use different method of tree traversal, maybe something like react fiber 14 | (sync! [this])) 15 | 16 | (defprotocol IManageNodes 17 | (dom-insert [this parent anchor]) 18 | (dom-first [this])) 19 | 20 | (defprotocol ITraverseNodes 21 | (managed-nodes [this])) 22 | 23 | (defprotocol IUpdatable 24 | (supports? [this next]) 25 | (dom-sync! [this next])) 26 | 27 | (defprotocol IHaveSlots 28 | (dom-slot [this id])) 29 | 30 | (defprotocol IAmStateful 31 | (invalidate! [this])) 32 | 33 | (defprotocol IDestructible 34 | (destroyed? [this]) 35 | (destroy! [this])) 36 | 37 | (defprotocol IHandleEvents 38 | (handle-event! [this ev-id e ev-args])) 39 | 40 | ;; root user api 41 | (defprotocol IDirectUpdate 42 | (update! [this next])) 43 | 44 | (defprotocol IScheduleUpdates 45 | (schedule-update! [this target]) 46 | (schedule-event! [this event-fn])) 47 | 48 | (defmulti set-attr* 49 | (fn [env node key oval nval] key) 50 | :default ::default) -------------------------------------------------------------------------------- /src/main/shadow/arborist/attributes.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.attributes 2 | (:require 3 | [goog.object :as gobj] 4 | [clojure.string :as str] 5 | [shadow.arborist.protocols :as p] 6 | )) 7 | 8 | ;; FIXME: keep this code short, due to the set-attr* multimethod nothing this uses will ever be removed 9 | 10 | (defn vec->class [v] 11 | (reduce 12 | (fn [s c] 13 | (cond 14 | (not c) 15 | s 16 | 17 | (not s) 18 | c 19 | 20 | :else 21 | (str s " " c))) 22 | nil 23 | v)) 24 | 25 | (defn map->class [m] 26 | (reduce-kv 27 | (fn [s ^not-native k v] 28 | (cond 29 | (not v) 30 | s 31 | 32 | (not s) 33 | (-name k) 34 | 35 | :else 36 | (str s " " (-name k)))) 37 | nil 38 | m)) 39 | 40 | ;; FIXME: behave like goog.dom.setProperties? 41 | ;; https://github.com/google/closure-library/blob/31e914b9ecc5c6918e2e6462cbbd4c77f90be753/closure/goog/dom/dom.js#L453 42 | (defmethod p/set-attr* ::p/default [env ^js node ^not-native key oval nval] 43 | (if nval 44 | (.setAttribute node (-name key) nval) 45 | (.removeAttribute node (-name key)))) 46 | 47 | (defmethod p/set-attr* :for [env ^js node _ oval nval] 48 | (set! node -htmlFor nval)) 49 | 50 | (defmethod p/set-attr* :style [env ^js node _ oval nval] 51 | (cond 52 | (map? nval) 53 | (let [style (.-style node)] 54 | (reduce-kv 55 | (fn [_ ^not-native k v] 56 | (gobj/set style (-name k) v)) 57 | nil 58 | nval)) 59 | 60 | (string? nval) 61 | (set! (.. node -style -cssText) nval) 62 | 63 | :else 64 | (throw (ex-info "invalid value for :class" {:node node :val nval})) 65 | )) 66 | 67 | (defmethod p/set-attr* :class [env ^js node _ oval nval] 68 | (cond 69 | (string? nval) 70 | (set! node -className nval) 71 | 72 | ;; FIXME: classlist? 73 | (vector? nval) 74 | (if-let [s (vec->class nval)] 75 | (set! node -className s) 76 | (set! node -className "")) 77 | 78 | (map? nval) 79 | (if-let [s (map->class nval)] 80 | (set! node -className s) 81 | ;; FIXME: removeAttribute? nil? 82 | (set! node -className "")) 83 | 84 | :else 85 | (throw (ex-info "invalid value for :class" {:node node :val nval})) 86 | )) 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/main/shadow/arborist.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist 2 | {:doc "Arborists generally focus on the health and safety of individual plants and trees." 3 | :definition "https://en.wikipedia.org/wiki/Arborist"} 4 | (:require-macros 5 | [shadow.arborist] 6 | [shadow.arborist.fragments]) 7 | (:require 8 | [shadow.arborist.protocols :as p] 9 | [shadow.arborist.fragments :as frag] 10 | [shadow.arborist.attributes :as attr] 11 | [shadow.arborist.components :as comp] 12 | [shadow.arborist.common :as common] 13 | [shadow.arborist.collections :as coll])) 14 | 15 | (deftype TreeScheduler [root ^:mutable update-pending?] 16 | p/IScheduleUpdates 17 | (schedule-update! [this component] 18 | (set! update-pending? true)) 19 | 20 | (schedule-event! [this event-fn] 21 | (event-fn) 22 | (when update-pending? 23 | (set! update-pending? false) 24 | (p/sync! root) 25 | (js/console.log "sync complete!" update-pending?)))) 26 | 27 | (deftype TreeRoot [container ^:mutable env ^:mutable root] 28 | p/ITraverseNodes 29 | (managed-nodes [this] 30 | (p/managed-nodes root)) 31 | 32 | p/IDirectUpdate 33 | (update! [this next] 34 | (if root 35 | (p/update! root next) 36 | (let [new-root (common/ManagedRoot. env nil nil)] 37 | (set! root new-root) 38 | (p/update! root next) 39 | (p/dom-insert root container nil) 40 | ))) 41 | 42 | p/ITreeNode 43 | (sync! [this] 44 | (p/sync! root)) 45 | 46 | p/IDestructible 47 | (destroy! [this] 48 | (when root 49 | (p/destroy! root)))) 50 | 51 | (defn dom-root 52 | ([container env] 53 | (let [root (TreeRoot. container nil nil) 54 | scheduler (TreeScheduler. root false) 55 | root-env (assoc env ::root root ::comp/scheduler scheduler)] 56 | (set! (.-env root) root-env) 57 | root)) 58 | ([container env init] 59 | (doto (dom-root container env) 60 | (p/update! init)))) 61 | 62 | (defn >> 63 | ([component props] 64 | {:pre [(or (map? component) (fn? component)) 65 | (map? props)]} 66 | (comp/ComponentNode. component props)) 67 | ;; FIXME: this works but shouldn't be used directly? 68 | #_([component props & children] 69 | (comp/ChildrenNode. 70 | (>> component props) 71 | (vec children)))) 72 | 73 | (defn << [& body] 74 | (throw (ex-info "<< can only be called used a macro" {}))) 75 | 76 | (defn fragment [& body] 77 | (throw (ex-info "fragment can only be called used a macro" {}))) 78 | 79 | (defn slot [env] 80 | (comp/slot env)) 81 | 82 | ;; FIXME: to be ->> friendly this should take coll last? 83 | (defn render-seq [coll key-fn render-fn] 84 | (coll/node coll key-fn render-fn)) 85 | 86 | (defn update! [x next] 87 | (p/update! x next)) 88 | 89 | (defn destroy! [root] 90 | (p/destroy! root)) 91 | 92 | js->clj -------------------------------------------------------------------------------- /src/main/shadow/arborist/common.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.common 2 | (:require [shadow.arborist.protocols :as p])) 3 | 4 | (defn marker [env] 5 | (js/document.createTextNode "")) 6 | 7 | (defn fragment-replace [old-managed new-managed] 8 | (let [first-node (p/dom-first old-managed) 9 | parent (.-parentNode first-node)] 10 | 11 | (p/dom-insert new-managed parent first-node) 12 | (p/destroy! old-managed))) 13 | 14 | (defn replace-managed [env old nval] 15 | (let [new (p/as-managed nval env)] 16 | (fragment-replace old new) 17 | new 18 | )) 19 | 20 | ;; swappable root 21 | (deftype ManagedRoot [env ^:mutable node ^:mutable val] 22 | p/IManageNodes 23 | (dom-first [this] 24 | (p/dom-first node)) 25 | 26 | (dom-insert [this parent anchor] 27 | (when-not node 28 | (throw (ex-info "root not initialized" {}))) 29 | 30 | (p/dom-insert node parent anchor)) 31 | 32 | p/ITreeNode 33 | (sync! [this] 34 | (p/sync! node)) 35 | 36 | p/ITraverseNodes 37 | (managed-nodes [this] 38 | (if-not node 39 | [] 40 | [node])) 41 | 42 | p/IDirectUpdate 43 | (update! [this next] 44 | (when (not= next val) 45 | (set! val next) 46 | (cond 47 | (not node) 48 | (let [el (p/as-managed val env)] 49 | (set! node el)) 50 | 51 | (p/supports? node next) 52 | (p/dom-sync! node next) 53 | 54 | :else 55 | (let [new (replace-managed env node next)] 56 | (set! node new))))) 57 | 58 | p/IDestructible 59 | (destroy! [this] 60 | (when node 61 | (p/destroy! node)))) 62 | 63 | (deftype ManagedText [env ^:mutable val node] 64 | p/IManageNodes 65 | (dom-first [this] node) 66 | 67 | (dom-insert [this parent anchor] 68 | (.insertBefore parent node anchor)) 69 | 70 | p/ITraverseNodes 71 | (managed-nodes [this] []) 72 | 73 | p/IUpdatable 74 | (supports? [this next] 75 | ;; FIXME: anything else? 76 | (or (string? next) 77 | (number? next) 78 | (nil? next))) 79 | 80 | (dom-sync! [this next] 81 | (when (not= val next) 82 | (set! val next) 83 | ;; https://twitter.com/_developit/status/1129093390883315712 84 | (set! node -data (str next))) 85 | :synced) 86 | 87 | p/IDestructible 88 | (destroy! [this] 89 | (.remove node))) 90 | 91 | (defn managed-text [env val] 92 | (ManagedText. env val (js/document.createTextNode (str val)))) 93 | 94 | (extend-protocol p/IConstruct 95 | string 96 | (as-managed [this env] 97 | (managed-text env this)) 98 | 99 | number 100 | (as-managed [this env] 101 | (managed-text env this)) 102 | 103 | ;; as a placeholder for (when condition (<< [:deep [:tree]])) 104 | nil 105 | (as-managed [this env] 106 | (managed-text env this))) 107 | -------------------------------------------------------------------------------- /src/main/shadow/arborist/interpreted.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.interpreted 2 | (:require [shadow.arborist.fragments :as frag] 3 | [shadow.arborist.protocols :as p] 4 | [clojure.string :as str])) 5 | 6 | (deftype ManagedVector 7 | [env 8 | ^:mutable node 9 | ^:mutable tag-kw 10 | ^:mutable attrs 11 | ^:mutable children 12 | ^:mutable src] 13 | 14 | p/IManageNodes 15 | (dom-first [this] node) 16 | (dom-insert [this parent anchor] 17 | (run! #(p/dom-insert % node nil) children) 18 | (.insertBefore parent node anchor)) 19 | 20 | p/ITraverseNodes 21 | (managed-nodes [this] children) 22 | 23 | p/IUpdatable 24 | (supports? [this next] 25 | (and (vector? next) 26 | (keyword-identical? tag-kw (get next 0)))) 27 | 28 | (dom-sync! [this [_ next-attrs :as next]] 29 | 30 | ;; FIXME: could be optimized 31 | (let [[next-attrs next-nodes] 32 | (if (map? next-attrs) 33 | [attrs (subvec next 2)] 34 | [nil (subvec next 1)])] 35 | 36 | (reduce-kv 37 | (fn [_ key nval] 38 | (let [oval (get attrs key)] 39 | (when (not= nval oval) 40 | (frag/set-attr env node key oval nval)))) 41 | nil 42 | next-attrs) 43 | 44 | ;; {:a 1 :x 1} vs {:a 1} 45 | ;; {:a 1} vs {:b 1} 46 | ;; should be uncommon but need to unset props that are no longer used 47 | (reduce-kv 48 | (fn [_ key oval] 49 | (when-not (contains? next-attrs key) 50 | (frag/set-attr env node key oval nil))) 51 | nil 52 | attrs) 53 | 54 | (let [oc (count children) 55 | nc (count next-nodes) 56 | 57 | ;; FIXME: transient? 58 | ;; update previous children 59 | next-children 60 | (reduce-kv 61 | (fn [c idx child] 62 | (if (>= idx nc) 63 | (do (p/destroy! child) 64 | c) 65 | (let [next (nth next-nodes idx)] 66 | (if (p/supports? child next) 67 | (do (p/dom-sync! child next) 68 | (conj c child)) 69 | (let [first (p/dom-first child) 70 | new-managed (p/as-managed next env)] 71 | (p/dom-insert new-managed node first) 72 | (p/destroy! child) 73 | (conj c new-managed)))))) 74 | [] 75 | children) 76 | 77 | ;; append if there were more children 78 | next-children 79 | (if-not (> nc oc) 80 | next-children 81 | (reduce 82 | (fn [c el] 83 | (let [new-managed (p/as-managed el env)] 84 | (p/dom-insert new-managed node nil) 85 | (conj c new-managed))) 86 | next-children 87 | (subvec next-nodes oc)))] 88 | 89 | (set! children next-children) 90 | (set! attrs next-attrs) 91 | (set! src next)))) 92 | 93 | p/IDestructible 94 | (destroy! [this] 95 | (.remove node) 96 | (run! #(p/destroy! %) children))) 97 | 98 | (defn just-tag [kw] 99 | (let [s (name kw)] 100 | (if-let [idx (str/index-of s "#")] 101 | (subs s 0 idx) 102 | (if-let [idx (str/index-of s ".")] 103 | (subs s 0 idx) 104 | s)))) 105 | 106 | (extend-type cljs.core/PersistentVector 107 | p/IConstruct 108 | (as-managed [[tag-kw attrs :as this] env] 109 | (let [[attrs children] 110 | (if (map? attrs) 111 | [attrs (subvec this 2)] 112 | [nil (subvec this 1)]) 113 | 114 | node 115 | (js/document.createElement (just-tag tag-kw)) 116 | 117 | ;; FIXME: support #id.class properly 118 | 119 | children 120 | (into [] (map #(p/as-managed % env)) children)] 121 | 122 | (reduce-kv 123 | (fn [_ key val] 124 | (frag/set-attr env node key nil val)) 125 | nil 126 | attrs) 127 | 128 | (ManagedVector. env node tag-kw attrs children this)))) 129 | -------------------------------------------------------------------------------- /src/main/shadow/arborist/collections.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.collections 2 | (:require 3 | [shadow.arborist.protocols :as p] 4 | [shadow.arborist.fragments :as frag])) 5 | 6 | (declare CollectionNode) 7 | 8 | (defn index-map ^not-native [key-vec] 9 | (persistent! (reduce-kv #(assoc! %1 %3 %2) (transient {}) key-vec))) 10 | 11 | (deftype ManagedCollection 12 | [env 13 | ^:mutable coll 14 | ^:mutable key-fn 15 | ^:mutable render-fn 16 | ^:mutable ^not-native items ;; map of {key managed} 17 | ^:mutable item-keys ;; vector of (key-fn item) 18 | ^:mutable item-vals ;; map of {key rendered} 19 | marker-before 20 | marker-after 21 | ] 22 | 23 | p/IManageNodes 24 | (dom-first [this] marker-before) 25 | 26 | (dom-insert [this parent anchor] 27 | (.insertBefore parent marker-before anchor) 28 | (run! #(p/dom-insert (get items %) parent anchor) item-keys) 29 | (.insertBefore parent marker-after anchor)) 30 | 31 | p/ITraverseNodes 32 | (managed-nodes [this] 33 | (mapv #(get items %) item-keys)) 34 | 35 | p/ITreeNode 36 | (sync! [this] 37 | (run! 38 | (fn [key] 39 | (let [item (get items key)] 40 | (p/sync! item))) 41 | item-keys)) 42 | 43 | p/IUpdatable 44 | (supports? [this next] 45 | (instance? CollectionNode next)) 46 | 47 | (dom-sync! [this ^CollectionNode next] 48 | (let [old-coll coll 49 | new-coll (vec (.-coll next)) ;; FIXME: could use into-array 50 | dom-parent (.-parentNode marker-after)] 51 | 52 | (when-not dom-parent 53 | (throw (ex-info "sync while not in dom?" {}))) 54 | 55 | (set! coll new-coll) 56 | (set! key-fn (.-key-fn next)) 57 | (set! render-fn (.-render-fn next)) 58 | 59 | (let [old-keys item-keys 60 | old-indexes (index-map old-keys) 61 | new-keys (into [] (map key-fn) coll) 62 | 63 | updated 64 | (loop [anchor marker-after 65 | idx (-> new-keys count dec) 66 | updated (transient #{})] 67 | (if (neg? idx) 68 | (persistent! updated) 69 | (let [key (nth new-keys idx) 70 | item (get items key) 71 | data (nth new-coll idx) 72 | updated (conj! updated key)] 73 | 74 | (if-not item 75 | ;; new item added to list, render normally and insert 76 | (let [rendered (render-fn data idx key) 77 | item (p/as-managed rendered env)] 78 | 79 | (p/dom-insert item (.-parentNode anchor) anchor) 80 | (set! item-vals (assoc item-vals key rendered)) 81 | (set! items (assoc items key item)) 82 | (recur (p/dom-first item) (dec idx) updated)) 83 | 84 | ;; item did exist, re-render and maybe move 85 | (let [rendered (render-fn data idx key)] 86 | 87 | ;; skip dom-sync if render result is equal 88 | (if (= rendered (get item-vals key)) 89 | (let [next-anchor (p/dom-first item)] 90 | 91 | ;; still may need to move though 92 | (when (not= idx (get old-indexes key)) 93 | (p/dom-insert item dom-parent anchor)) 94 | 95 | (recur next-anchor (dec idx) updated)) 96 | 97 | (do (set! item-vals (assoc item-vals key rendered)) 98 | (if (p/supports? item rendered) 99 | ;; update in place if supported 100 | (do (p/dom-sync! item rendered) 101 | (let [next-anchor (p/dom-first item)] 102 | 103 | ;; FIXME: this is probably not ideal 104 | (when (not= idx (get old-indexes key)) 105 | (p/dom-insert item dom-parent anchor)) 106 | 107 | (recur next-anchor (dec idx) updated))) 108 | 109 | ;; not updateable, swap 110 | (let [new-item (p/as-managed rendered env)] 111 | (set! items (assoc items key new-item)) 112 | (p/dom-insert new-item dom-parent anchor) 113 | (p/destroy! item) 114 | 115 | (recur (p/dom-first new-item) (dec idx) updated) 116 | )))))))))] 117 | 118 | (set! item-keys new-keys) 119 | 120 | ;; remove old items/render results 121 | (reduce-kv 122 | (fn [_ key item] 123 | (when-not (contains? updated key) 124 | (p/destroy! item) 125 | (set! items (dissoc items key)) 126 | (set! item-vals (dissoc item-vals key)))) 127 | nil 128 | items))) 129 | :synced) 130 | 131 | p/IDestructible 132 | (destroy! [this] 133 | (.remove marker-before) 134 | (when items 135 | (reduce-kv 136 | (fn [_ _ item] 137 | (p/destroy! item)) 138 | nil 139 | items)) 140 | (.remove marker-after))) 141 | 142 | (deftype CollectionNode [coll key-fn render-fn] 143 | p/IConstruct 144 | (as-managed [this env] 145 | (let [coll (vec coll) ;; FIXME: could use into-array, colls are never modified again, only used to look stuff up by index 146 | len (count coll) 147 | marker-before (js/document.createComment "coll-start") 148 | marker-after (js/document.createComment "coll-end")] 149 | 150 | (loop [idx 0 151 | items (transient {}) 152 | keys (transient []) 153 | vals (transient {})] 154 | 155 | (if (>= idx len) 156 | (ManagedCollection. 157 | env 158 | coll 159 | key-fn 160 | render-fn 161 | (persistent! items) 162 | (persistent! keys) 163 | (persistent! vals) 164 | marker-before 165 | marker-after) 166 | 167 | (let [val (nth coll idx) 168 | key (key-fn val) 169 | rendered (render-fn val idx key) 170 | managed (p/as-managed rendered env)] 171 | 172 | (recur 173 | (inc idx) 174 | (assoc! items key managed) 175 | (conj! keys key) 176 | (assoc! vals key rendered))))))) 177 | 178 | IEquiv 179 | (-equiv [this ^CollectionNode other] 180 | (and (instance? CollectionNode other) 181 | ;; could be a keyword, can't use identical? 182 | (keyword-identical? key-fn (.-key-fn other)) 183 | ;; FIXME: this makes it never equal if fn is created in :render fn 184 | (identical? render-fn (.-render-fn other)) 185 | ;; compare coll last since its pointless if the others changed and typically more expensive to compare 186 | (= coll (.-coll other))))) 187 | 188 | (defn node [coll key-fn render-fn] 189 | {:pre [(sequential? coll) 190 | (ifn? key-fn) 191 | (fn? render-fn)]} 192 | (CollectionNode. coll key-fn render-fn)) -------------------------------------------------------------------------------- /src/main/shadow/arborist/fragments.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.fragments 2 | (:require 3 | [shadow.arborist.protocols :as p] 4 | [shadow.arborist.common :as common] 5 | [shadow.arborist.components :as comp])) 6 | 7 | (defonce fragment-cache-ref (atom {})) 8 | 9 | (defn ^:dev/before-load clear-fragments! [] 10 | (reset! fragment-cache-ref {})) 11 | 12 | (defn array-equiv [a b] 13 | (let [al (alength a) 14 | bl (alength b)] 15 | (when (identical? al bl) 16 | (loop [i 0] 17 | (when (< i al) 18 | (when (= (aget a i) (aget b i)) 19 | (recur (inc i)))))))) 20 | 21 | ;; sometimes need to keep roots and elements in sync 22 | ;; FIXME: do 2 array really make sense? 23 | ;; could just be one array with indexes 24 | ;; and one actual array with elements? 25 | ;; roots needs to be kept in sync otherwise it prevents GC of detroyed elements 26 | (defn array-swap [a old swap] 27 | (let [idx (.indexOf a old)] 28 | (when-not (neg? idx) 29 | (aset a idx swap)))) 30 | 31 | (deftype FragmentController [frag-id create-fn update-fn] 32 | p/IControlFragment 33 | (fragment-build [this env vals] 34 | (create-fn env vals)) 35 | 36 | (fragment-update [this env roots nodes ovals nvals] 37 | (update-fn env roots nodes ovals nvals))) 38 | 39 | (declare fragment-node?) 40 | 41 | (deftype ManagedFragment 42 | [env 43 | ^:mutable ^not-native control 44 | ^:mutable vals 45 | marker 46 | roots 47 | nodes] 48 | 49 | p/IManageNodes 50 | (dom-first [this] marker) 51 | 52 | (dom-insert [this parent anchor] 53 | (.insertBefore parent marker anchor) 54 | (dotimes [idx (alength roots)] 55 | (let [root (aget roots idx)] 56 | (if (satisfies? p/IManageNodes root) 57 | (p/dom-insert root parent anchor) 58 | (.insertBefore parent root anchor))))) 59 | 60 | p/ITraverseNodes 61 | (managed-nodes [this] 62 | (into [] (filter #(satisfies? p/IManageNodes %)) nodes)) 63 | 64 | p/ITreeNode 65 | (sync! [this] 66 | (dotimes [idx (alength nodes)] 67 | (let [node (aget nodes idx)] 68 | (when (satisfies? p/ITreeNode node) 69 | (p/sync! node))))) 70 | 71 | p/IUpdatable 72 | (supports? [this ^FragmentNode next] 73 | (and (fragment-node? next) 74 | (identical? control (.-control next)))) 75 | 76 | (dom-sync! [this ^FragmentNode next] 77 | (let [nvals (.-vals next)] 78 | ;; impl decides what to update, no need to compare 79 | (p/fragment-update control env roots nodes vals nvals) 80 | (set! vals nvals)) 81 | :synced) 82 | 83 | p/IDestructible 84 | (destroy! [this] 85 | (.remove marker) 86 | (dotimes [x (alength nodes)] 87 | (let [el (aget nodes x)] 88 | (if (satisfies? p/IDestructible el) 89 | (p/destroy! el) 90 | (.remove el)))) 91 | 92 | ;; FIXME: only necessary because of top level text nodes which aren't in nodes 93 | ;; might be better to just add them into nodes instead of leaving them out in the first place 94 | (dotimes [x (alength roots)] 95 | (let [el (aget roots x)] 96 | (when-not (satisfies? p/IDestructible el) 97 | (.remove el)))) 98 | 99 | (set! (.-length roots) 0) 100 | (set! (.-length nodes) 0))) 101 | 102 | (deftype FragmentNode [frag-id vals control] 103 | p/IConstruct 104 | (as-managed [_ env] 105 | (let [state (p/fragment-build control env vals)] 106 | (ManagedFragment. env control vals (common/marker env) (aget state 0) (aget state 1)))) 107 | 108 | IEquiv 109 | (-equiv [this ^FragmentNode other] 110 | (and (instance? FragmentNode other) 111 | (identical? frag-id (.-frag-id other)) 112 | (identical? control (.-control other)) 113 | (array-equiv vals (.-vals other))))) 114 | 115 | (defn fragment-node? [thing] 116 | (instance? FragmentNode thing)) 117 | 118 | ;; 119 | ;; called from macro 120 | ;; 121 | 122 | (defn fragment-get [frag-id vals] 123 | (when-let [control (get @fragment-cache-ref frag-id)] 124 | (FragmentNode. frag-id vals control))) 125 | 126 | (defn fragment-reg [frag-id vals create-fn update-fn] 127 | (let [control (FragmentController. frag-id create-fn update-fn)] 128 | (swap! fragment-cache-ref assoc frag-id control) 129 | (FragmentNode. frag-id vals control))) 130 | 131 | ;; FIXME: should maybe take ::document from env 132 | ;; not sure under which circumstance this would ever need a different document instance though 133 | (defn create-element 134 | ;; inlined version is longer than the none inline version 135 | ;; {:jsdoc ["@noinline"]} 136 | [env ^not-native type] ;; kw 137 | (js/document.createElement (-name type))) 138 | 139 | (defn create-text 140 | ;; {:jsdoc ["@noinline"]} 141 | [env text] 142 | (js/document.createTextNode text)) 143 | 144 | (defn set-attr [env node key oval nval] 145 | ;; FIXME: cljs "bug" that will always emit the property check we know isn't required 146 | ;; (set-attr* env node key val) 147 | (.cljs$core$IFn$_invoke$arity$5 p/set-attr* env node key oval nval)) 148 | 149 | (defn append-child 150 | ;; {:jsdoc ["@noinline"]} 151 | [parent child] 152 | (.appendChild parent child)) 153 | 154 | (defn create-managed [env other] 155 | ;; FIXME: validate that return value implements the proper protocols 156 | (p/as-managed other env)) 157 | 158 | 159 | ;; called by macro generated code 160 | (defn append-managed [parent other] 161 | (when-not (satisfies? p/IUpdatable other) 162 | (throw (ex-info "cannot append-managed" {:parent parent :other other}))) 163 | (p/dom-insert other parent nil)) 164 | 165 | ;; called by macro generated code 166 | (defn update-managed [env roots nodes idx oval nval] 167 | ;; FIXME: should this even compare oval/nval? 168 | ;; comparing the array in fragment handles (which may contain other handles) might become expensive? 169 | ;; comparing 100 items to find the 99th didn't match 170 | ;; will compare 99 items again individually later in the code? 171 | ;; if everything is equal however a bunch of code can be skipped? 172 | 173 | ;; FIXME: actually benchmark in an actual app 174 | (when (not= oval nval) 175 | (let [el (aget nodes idx)] 176 | (if (p/supports? el nval) 177 | (p/dom-sync! el nval) 178 | (let [next (common/replace-managed env el nval)] 179 | (array-swap roots el next) 180 | (aset nodes idx next) 181 | ))))) 182 | 183 | ;; called by macro generated code 184 | (defn update-attr [env nodes idx ^not-native attr oval nval] 185 | (when (not= oval nval) 186 | (let [el (aget nodes idx)] 187 | (set-attr env el attr oval nval)))) 188 | 189 | (defn component-create [env component attrs] 190 | {:pre [(map? attrs)]} 191 | (comp/component-create env component attrs)) 192 | 193 | (defn component-update [env roots nodes idx oc nc oa na] 194 | {:pre [(map? na)]} 195 | (let [comp (aget nodes idx) 196 | tmp (comp/->ComponentNode nc na)] 197 | (if (p/supports? comp tmp) 198 | (p/dom-sync! comp tmp) 199 | (let [new (common/replace-managed env oc tmp)] 200 | (array-swap roots comp new) 201 | (aset nodes idx new) 202 | )))) 203 | 204 | (defn component-append [component child] 205 | (let [slot (p/dom-slot component :default)] 206 | (if-not (satisfies? p/IManageNodes child) 207 | (.insertBefore (.-parentNode slot) child slot) 208 | (p/dom-insert child (.-parentNode slot) slot)))) 209 | 210 | -------------------------------------------------------------------------------- /src/main/shadow/arborist/components.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.components 2 | (:require 3 | [goog.object :as gobj] 4 | [shadow.arborist.common :as common] 5 | [shadow.arborist.protocols :as p])) 6 | 7 | (declare component-node?) 8 | 9 | (defn safe-inc [x] 10 | (if (nil? x) 11 | 0 12 | (inc x))) 13 | 14 | (defonce component-id-seq (atom 0)) 15 | 16 | (defn next-component-id [] 17 | (swap! component-id-seq inc)) 18 | 19 | (set! *warn-on-infer* false) 20 | 21 | (deftype ManagedComponent 22 | [parent-env 23 | config 24 | ^:mutable props 25 | ^:mutable ^not-native root ;; must not be modified after init 26 | ^:mutable component-env ;; must not be modified after init 27 | ^:mutable render-fn 28 | ^:mutable state ;; FIXME: what if there was no state? 29 | ^:mutable dirty? 30 | ^:mutable destroyed? 31 | slots ;; map of {slot-id target-node} 32 | ] 33 | 34 | p/IManageNodes 35 | (dom-first [this] 36 | (p/dom-first root)) 37 | 38 | (dom-insert [this parent anchor] 39 | (p/dom-insert root parent anchor)) 40 | 41 | ;; FIXME: this could just delegate to the root? 42 | p/ITraverseNodes 43 | (managed-nodes [this] 44 | [root]) 45 | 46 | p/IHaveSlots 47 | (dom-slot [this id] 48 | (let [slot (get slots id)] 49 | (when-not slot 50 | (throw (ex-info "component did not define slot" {:id id :this this}))) 51 | 52 | (when-not (.-parentNode slot) 53 | (throw (ex-info "slot was not added in render" {:id id}))) 54 | 55 | slot)) 56 | 57 | p/IUpdatable 58 | (supports? [this next] 59 | (and (component-node? next) 60 | (let [other (.-component ^ComponentNode next)] 61 | (if (fn? other) 62 | (identical? render-fn other) 63 | (identical? config other))))) 64 | 65 | (dom-sync! [this ^ComponentNode next] 66 | ;; FIXME: let (:should-update config) decide if update should happen or not? 67 | (when (or dirty? (not= props (.-props next))) 68 | (let [prev-props props 69 | next-props (.-props next)] 70 | (set! props next-props) 71 | (.component-render! this) 72 | ;; FIXME: did-update before/after props 73 | )) 74 | :synced) 75 | 76 | p/IAmStateful 77 | (invalidate! [this] 78 | (set! dirty? true) 79 | (p/schedule-update! (::scheduler parent-env) this)) 80 | 81 | p/ITreeNode 82 | (sync! [this] 83 | (when dirty? 84 | (.component-render! this)) 85 | 86 | (p/sync! root)) 87 | 88 | p/IHandleEvents 89 | (handle-event! [this ev-id e ev-args] 90 | ;; fn-sig should be (fn [env e ...]) 91 | ;; but some generic event handlers may want to know the ev-id 92 | ;; but putting that in there seems messy 93 | ;; (fn [env ev-id e ...]) is too noisy 94 | ;; (fn [env e ev-id ...]) sucks with args 95 | ;; most event handlers probably won't care so just adding it to e 96 | (gobj/set e "__shadow$ev" ev-id) 97 | 98 | (when-let [handler (get config ev-id)] 99 | (run! 100 | #(apply %1 component-env e ev-args) 101 | (if (fn? handler) 102 | [handler] 103 | handler))) 104 | 105 | (when-let [parent (::component parent-env)] 106 | (p/handle-event! parent ev-id e ev-args))) 107 | 108 | p/IDestructible 109 | (destroyed? [this] 110 | destroyed?) 111 | 112 | (destroy! [this] 113 | (set! destroyed? true) 114 | 115 | (when-let [handler (:will-destroy config)] 116 | (handler component-env state)) 117 | 118 | (p/destroy! root)) 119 | 120 | Object 121 | ;; can't do this in the ComponentNode.as-managed since we need the this pointer 122 | (component-init! [^ComponentInstance this] 123 | (let [{:keys [init-state]} config 124 | 125 | child-env 126 | (-> parent-env 127 | (update ::depth safe-inc) 128 | (assoc ::parent (::component parent-env) 129 | ::dom-refs (atom {}) 130 | ::component-id (next-component-id) 131 | ::component this 132 | ::config config)) 133 | 134 | init-state 135 | (cond 136 | (fn? init-state) 137 | (init-state props) 138 | 139 | (some? init-state) 140 | init-state 141 | 142 | :else 143 | {})] 144 | 145 | (set! component-env child-env) 146 | (set! root (common/ManagedRoot. child-env nil nil)) 147 | (set! state init-state) 148 | (.component-render! this))) 149 | 150 | (component-render! [^ComponentInstance this] 151 | (let [frag (render-fn component-env props state)] 152 | (set! dirty? false) 153 | (p/update! root frag)))) 154 | 155 | (set! *warn-on-infer* true) 156 | 157 | (defn component-create [env component props] 158 | ;; FIXME: have component declare them properly 159 | (let [slots {:default (common/marker env)}] 160 | 161 | (cond 162 | (map? component) 163 | (doto (ManagedComponent. env component props nil nil (:render component) nil false false slots) 164 | (.component-init!)) 165 | 166 | (fn? component) 167 | (doto (ManagedComponent. env {} props nil nil component nil false false slots) 168 | (.component-init!))))) 169 | 170 | (deftype ComponentNode [component props] 171 | p/IConstruct 172 | (as-managed [this env] 173 | (component-create env component props)) 174 | 175 | IEquiv 176 | (-equiv [this ^ComponentNode other] 177 | (and (instance? ComponentNode other) 178 | (identical? component (.-component other)) 179 | (= props (.-props other))))) 180 | 181 | (defn component-node? [x] 182 | (instance? ComponentNode x)) 183 | 184 | (deftype SlotNode [node] 185 | p/IConstruct 186 | (as-managed [this env] 187 | this) 188 | 189 | p/IManageNodes 190 | (dom-first [this] node) 191 | (dom-insert [this parent anchor] 192 | (when (.-parentNode node) 193 | (throw (ex-info "slot already in document" {}))) 194 | 195 | (.insertBefore parent node anchor)) 196 | 197 | p/ITraverseNodes 198 | (managed-nodes [this] []) 199 | 200 | p/IUpdatable 201 | (supports? [this ^SlotNode other] 202 | (and (instance? SlotNode other) 203 | (identical? node (.-node other)))) 204 | 205 | (dom-sync! [this other]) 206 | 207 | p/IDestructible 208 | (destroy! [this] 209 | (.remove node))) 210 | 211 | (defn slot [{::keys [component] :as env}] 212 | (let [slots (.-slots component)] 213 | (SlotNode. (get slots :default)))) 214 | 215 | (declare ChildrenNode) 216 | 217 | (deftype ManagedChildren 218 | [env 219 | ^:mutable component 220 | ^:mutable children 221 | ^:mutable nodes] 222 | 223 | p/IManageNodes 224 | (dom-first [this] 225 | (p/dom-first component)) 226 | 227 | (dom-insert [this parent anchor] 228 | (p/dom-insert component parent anchor) 229 | (doseq [node nodes] 230 | (let [slot (p/dom-slot component :default)] 231 | (p/dom-insert node (.-parentNode slot) slot)))) 232 | 233 | p/ITraverseNodes 234 | (managed-nodes [this] 235 | nodes) 236 | 237 | p/IUpdatable 238 | (supports? [this ^ChildrenNode next] 239 | (and (instance? ChildrenNode next) 240 | (p/supports? component (.-component-node next)))) 241 | 242 | (dom-sync! [this ^ChildrenNode next] 243 | (p/dom-sync! component (.-component-node next)) 244 | 245 | (let [old-children children 246 | new-children (.-children next) 247 | 248 | oc (count old-children) 249 | nc (count new-children) 250 | m (js/Math.min oc nc)] 251 | 252 | (dotimes [i m] 253 | (let [old (nth old-children i) 254 | new (nth new-children i)] 255 | (when (not= old new) 256 | 257 | (let [node (nth nodes i)] 258 | (if (p/supports? node new) 259 | (p/dom-sync! node new) 260 | (let [new-node (common/replace-managed env node new)] 261 | (set! nodes (assoc nodes i new-node)))))))) 262 | 263 | (cond 264 | ;; less children 265 | (> oc nc) 266 | (throw (ex-info "TBD, changed number of children" {})) 267 | 268 | ;; more children 269 | (> nc oc) 270 | (throw (ex-info "TBD, changed number of children" {})) 271 | ))) 272 | 273 | p/IDestructible 274 | (destroy! [this] 275 | (run! #(p/destroy! %) nodes) 276 | (p/destroy! component))) 277 | 278 | 279 | (deftype ChildrenNode [component-node children] 280 | p/IConstruct 281 | (as-managed [this env] 282 | (let [comp 283 | (p/as-managed component-node env) 284 | 285 | nodes 286 | (into [] (map #(p/as-managed % env) children))] 287 | 288 | (ManagedChildren. env comp children nodes))) 289 | 290 | IEquiv 291 | (-equiv [this ^ChildrenNode other] 292 | (and (instance? ComponentNode other) 293 | (= component-node (.-component-node other)) 294 | (= children (.-children other))))) 295 | 296 | (defn call-event-fn [{::keys [component scheduler] :as env} ev-id e ev-args] 297 | (when-not component 298 | (throw (ex-info "event handlers can only be used in components" {:env env :ev-id ev-id :e e :ev-args ev-args}))) 299 | 300 | (p/schedule-event! scheduler #(p/handle-event! component ev-id e ev-args))) 301 | 302 | (defn event-attr [env node event oval [ev-id & ev-args :as nval]] 303 | 304 | (when ^boolean js/goog.DEBUG 305 | (when-not (vector? nval) 306 | (throw (ex-info "event handler expects a vector arg" {:event event :node node :nval nval}))) 307 | 308 | (when-not (keyword? ev-id) 309 | (throw (ex-info "event handler must start with [::keyword ...]" {:event event :node node :nval nval})))) 310 | 311 | 312 | (let [ev-key (str "__shadow$" (name event))] 313 | (when-let [ev-fn (gobj/get node ev-key)] 314 | (.removeEventListener node ev-fn)) 315 | 316 | ;(js/console.log "adding ev fn" val) 317 | 318 | (let [ev-fn #(call-event-fn env ev-id % ev-args) 319 | ev-opts #js {}] 320 | 321 | ;; FIXME: need to track if once already happened. otherwise may re-attach and actually fire more than once 322 | ;; but it should be unlikely to have a changing val with ^:once? 323 | (when-let [m (meta nval)] 324 | (when (:once m) 325 | (gobj/set ev-opts "once" true)) 326 | 327 | (when (:passive m) 328 | (gobj/set ev-opts "passive" true))) 329 | 330 | ;; FIXME: ev-opts are not supported by all browsers 331 | ;; closure lib probably has something to handle that 332 | (.addEventListener node (name event) ev-fn ev-opts) 333 | 334 | (gobj/set node ev-key ev-fn)))) 335 | 336 | (defmethod p/set-attr* :on-click [env node _ oval nval] 337 | (event-attr env node :click oval nval)) 338 | 339 | (defmethod p/set-attr* :on-dblclick [env node _ oval nval] 340 | (event-attr env node :dblclick oval nval)) 341 | 342 | (defmethod p/set-attr* :on-keydown [env node _ oval nval] 343 | (event-attr env node :keydown oval nval)) 344 | 345 | (defmethod p/set-attr* :on-change [env node _ oval nval] 346 | (event-attr env node :change oval nval)) 347 | 348 | (defmethod p/set-attr* :on-blur [env node _ oval nval] 349 | (event-attr env node :blur oval nval)) 350 | 351 | (defmethod p/set-attr* :shadow.arborist/ref [{::keys [dom-refs] :as env} node _ oval nval] 352 | (when-not dom-refs 353 | (throw (ex-info "ref used outside component" {:val nval :env env}))) 354 | (when (and oval (not= oval nval)) 355 | (swap! dom-refs dissoc oval)) 356 | (swap! dom-refs assoc nval node)) 357 | 358 | -------------------------------------------------------------------------------- /src/main/shadow/arborist/fragments.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.arborist.fragments 2 | (:require [clojure.string :as str])) 3 | 4 | (defn parse-tag [spec] 5 | (let [spec (name spec) 6 | fdot (.indexOf spec ".") 7 | fhash (.indexOf spec "#")] 8 | (cond 9 | (and (= -1 fdot) (= -1 fhash)) 10 | [spec nil nil] 11 | 12 | (= -1 fhash) 13 | [(subs spec 0 fdot) 14 | nil 15 | (str/replace (subs spec (inc fdot)) #"\." " ")] 16 | 17 | (= -1 fdot) 18 | [(subs spec 0 fhash) 19 | (subs spec (inc fhash)) 20 | nil] 21 | 22 | (> fhash fdot) 23 | (throw (str "cant have id after class?" spec)) 24 | 25 | :else 26 | [(subs spec 0 fhash) 27 | (subs spec (inc fhash) fdot) 28 | (str/replace (.substring spec (inc fdot)) #"\." " ")]))) 29 | 30 | (defn const? [thing] 31 | (or (string? thing) 32 | (number? thing) 33 | (boolean? thing) 34 | (keyword? thing) 35 | (= thing 'nil) 36 | (and (vector? thing) (every? const? thing)) 37 | (and (map? thing) 38 | (reduce-kv 39 | (fn [r k v] 40 | (if-not (and (const? k) (const? v)) 41 | (reduced false) 42 | r)) 43 | true 44 | thing)))) 45 | 46 | (defn next-el-id [{:keys [el-seq-ref] :as env}] 47 | (swap! el-seq-ref inc)) 48 | 49 | (defn make-code [{:keys [code-ref] :as env} code {:keys [element-id] :as extra}] 50 | ;; FIXME: de-duping code may eliminate wanted side effects? 51 | ;; (<< [:div (side-effect) (side-effect)]) 52 | ;; this is probably fine since the side-effect should be done elsewhere anyways 53 | (let [id (or (get @code-ref code) 54 | (let [next-id (count @code-ref)] 55 | (swap! code-ref assoc code next-id) 56 | next-id))] 57 | 58 | (merge 59 | extra 60 | {:op :code-ref 61 | :parent (:parent env) 62 | :sym (if element-id 63 | (symbol (str "d" element-id)) ;; dynamic "elements" 64 | (gensym)) 65 | :ref-id id}))) 66 | 67 | (declare analyze-node) 68 | 69 | (defn analyze-component [env [tag component attrs & children :as el]] 70 | (assert (> (count el) 2)) 71 | 72 | (let [id (next-el-id env) 73 | el-sym (symbol (str "c" id)) 74 | 75 | child-env 76 | (assoc env :parent [:component el-sym])] 77 | 78 | {:op :component 79 | :parent (:parent env) 80 | :component (make-code env component {}) 81 | :attrs (make-code env attrs {}) 82 | :element-id id 83 | :sym el-sym 84 | :src el 85 | :children (into [] (map #(analyze-node child-env %)) children)})) 86 | 87 | (defn with-loc [{:keys [src] :as ast} form] 88 | (if-not src 89 | form 90 | (let [m (meta src)] 91 | (if-not m 92 | form 93 | (with-meta form m))))) 94 | 95 | (defn analyze-dom-element [env [tag-kw attrs :as el]] 96 | (let [[attrs children] 97 | (if (and attrs (map? attrs)) 98 | [attrs (subvec el 2)] 99 | [nil (subvec el 1)]) 100 | 101 | [tag html-id html-class] 102 | (parse-tag tag-kw) 103 | 104 | id (next-el-id env) 105 | 106 | el-sym (symbol (str "el" id "_" tag)) 107 | 108 | _ (when (and html-id (:id attrs)) 109 | (throw (ex-info "cannot have :id attribute AND el#id" {:tag-kw tag-kw :attrs attrs}))) 110 | 111 | _ (when (and html-class (:class attrs)) 112 | (throw (ex-info "cannot have :class attribute AND el.class" {:tag-kw tag-kw :attrs attrs}))) 113 | 114 | attrs 115 | (-> attrs 116 | (cond-> 117 | html-id 118 | (assoc :id html-id) 119 | 120 | html-class 121 | (assoc :class html-class))) 122 | 123 | tag (keyword (namespace tag-kw) tag) 124 | 125 | attr-ops 126 | (->> attrs 127 | (map (fn [[attr-key attr-value]] 128 | (if (const? attr-value) 129 | {:op :static-attr 130 | :el el-sym 131 | :element-id id 132 | :attr attr-key 133 | :value attr-value 134 | :src attrs} 135 | 136 | ;; FIXME: this could be smarter and pre-generate "partially" dynamic attrs 137 | ;; :class ["hello" "world" (when x "foo")] 138 | ;; could build ["hello "world" nil] once 139 | ;; and then (assoc the-const 2 (when x "foo")) before passing it along 140 | ;; :style {:color "red" :font-size x} 141 | ;; (assoc the-const :font-size x) 142 | ;; probably complete overkill but could be fun 143 | {:op :dynamic-attr 144 | :el el-sym 145 | :element-id id 146 | :attr attr-key 147 | :value (make-code env attr-value {}) 148 | :src attrs} 149 | ))) 150 | (into [])) 151 | 152 | child-env 153 | (assoc env :parent [:element el-sym])] 154 | 155 | {:op :element 156 | :parent (:parent env) 157 | :element-id id 158 | :sym el-sym 159 | :tag tag 160 | :src el 161 | :children 162 | (-> [] 163 | (into attr-ops) 164 | (into (map #(analyze-node child-env %)) children))})) 165 | 166 | (defn analyze-element [env el] 167 | (assert (pos? (count el))) 168 | 169 | (let [tag-kw (nth el 0)] 170 | (when-not (keyword? tag-kw) 171 | (throw (ex-info "vector must start with a keyword" {:el el}))) 172 | 173 | (if (= :> tag-kw) 174 | (analyze-component env el) 175 | (analyze-dom-element env el)))) 176 | 177 | (defn analyze-text [env node] 178 | {:op :text 179 | :parent (:parent env) 180 | :sym (gensym) 181 | :text node}) 182 | 183 | (defn analyze-node [env node] 184 | (cond 185 | (vector? node) 186 | (analyze-element env node) 187 | 188 | (string? node) 189 | (analyze-text env node) 190 | 191 | (number? node) 192 | (analyze-text env (str node)) 193 | 194 | :else 195 | (make-code env node {:element-id (next-el-id env)}))) 196 | 197 | (defn reduce-> [init reduce-fn coll] 198 | (reduce reduce-fn init coll)) 199 | 200 | (defn make-build-impl [ast] 201 | (let [this-sym (gensym "this") 202 | env-sym (with-meta (gensym "env") {:tag 'not-native}) 203 | vals-sym (with-meta (gensym "vals") {:tag 'not-native}) 204 | 205 | {:keys [bindings mutations return nodes] :as result} 206 | (reduce 207 | (fn step-fn [env {:keys [op sym parent] :as ast}] 208 | (let [[parent-type parent-sym] parent] 209 | (case op 210 | :element 211 | (-> env 212 | (update :bindings conj sym (with-loc ast `(create-element ~env-sym ~(:tag ast)))) 213 | (update :return conj sym) 214 | (cond-> 215 | (and parent-sym (= parent-type :element)) 216 | (update :mutations conj (with-loc ast `(append-child ~parent-sym ~sym))) 217 | 218 | (and parent-sym (= parent-type :component)) 219 | (update :mutations conj (with-loc ast `(component-append ~parent-sym ~sym))) 220 | 221 | (not parent-sym) 222 | (update :nodes conj sym)) 223 | (reduce-> step-fn (:children ast)) 224 | ) 225 | 226 | :component 227 | (-> env 228 | (update :bindings conj sym 229 | (with-loc ast 230 | `(component-create ~env-sym 231 | (aget ~vals-sym ~(-> ast :component :ref-id)) 232 | (aget ~vals-sym ~(-> ast :attrs :ref-id))))) 233 | (update :return conj sym) 234 | (cond-> 235 | parent-sym 236 | (update :mutations conj (with-loc ast `(append-managed ~parent-sym ~sym))) 237 | (not parent-sym) 238 | (update :nodes conj sym)) 239 | (reduce-> step-fn (:children ast))) 240 | 241 | ;; text nodes can never mutate. no need to return them 242 | ;; 243 | :text 244 | (-> env 245 | (update :bindings conj sym `(create-text ~env-sym ~(:text ast))) 246 | (cond-> 247 | parent-sym 248 | (update :mutations conj `(append-child ~parent-sym ~sym)) 249 | 250 | (not parent-sym) 251 | (update :nodes conj sym) 252 | )) 253 | 254 | :code-ref 255 | (-> env 256 | (update :bindings conj sym (with-loc ast `(create-managed ~env-sym (aget ~vals-sym ~(:ref-id ast))))) 257 | (cond-> 258 | parent-sym 259 | (update :mutations conj (with-loc ast `(append-managed ~parent-sym ~sym))) 260 | 261 | (not parent-sym) 262 | (update :nodes conj sym)) 263 | (update :return conj sym)) 264 | 265 | :static-attr 266 | (-> env 267 | (update :mutations conj (with-loc ast `(set-attr ~env-sym ~(:el ast) ~(:attr ast) nil ~(:value ast))))) 268 | 269 | :dynamic-attr 270 | (-> env 271 | (update :mutations conj (with-loc ast `(set-attr ~env-sym ~(:el ast) ~(:attr ast) nil (aget ~vals-sym ~(-> ast :value :ref-id)))) 272 | )))) 273 | ) 274 | {:bindings [] 275 | :mutations [] 276 | :return [] 277 | :nodes []} 278 | ast)] 279 | 280 | 281 | `(fn [~env-sym ~vals-sym] 282 | (let [~@bindings] 283 | ~@mutations 284 | (cljs.core/array (cljs.core/array ~@nodes) (cljs.core/array ~@return)))))) 285 | 286 | (defn make-update-impl [ast] 287 | (let [this-sym (gensym "this") 288 | env-sym (gensym "env") 289 | nodes-sym (gensym "nodes") 290 | roots-sym (gensym "roots") 291 | oldv-sym (gensym "oldv") 292 | newv-sym (gensym "newv") 293 | 294 | {:keys [bindings mutations return nodes] :as result} 295 | (reduce 296 | (fn step-fn [env {:keys [op sym element-id] :as ast}] 297 | (case op 298 | :element 299 | (-> env 300 | (assoc-in [:sym->id sym] element-id) 301 | (reduce-> step-fn (:children ast))) 302 | 303 | :component 304 | (-> env 305 | (update :mutations conj 306 | (with-loc ast 307 | `(component-update 308 | ~env-sym 309 | ~roots-sym 310 | ~nodes-sym 311 | ~(:element-id ast) 312 | (aget ~oldv-sym ~(-> ast :component :ref-id)) 313 | (aget ~newv-sym ~(-> ast :component :ref-id)) 314 | (aget ~oldv-sym ~(-> ast :attrs :ref-id)) 315 | (aget ~newv-sym ~(-> ast :attrs :ref-id))))) 316 | (reduce-> step-fn (:children ast))) 317 | 318 | :text 319 | env 320 | 321 | :code-ref 322 | (-> env 323 | (update :mutations conj 324 | (let [ref-id (:ref-id ast)] 325 | (with-loc ast 326 | `(update-managed 327 | ~env-sym 328 | ~roots-sym 329 | ~nodes-sym 330 | ~(:element-id ast) 331 | (aget ~oldv-sym ~ref-id) 332 | (aget ~newv-sym ~ref-id)))))) 333 | 334 | :static-attr 335 | env 336 | 337 | :dynamic-attr 338 | (let [ref-id (-> ast :value :ref-id) 339 | form 340 | `(update-attr ~env-sym ~nodes-sym ~element-id ~(:attr ast) (aget ~oldv-sym ~ref-id) (aget ~newv-sym ~ref-id))] 341 | 342 | (-> env 343 | (update :mutations conj form)))) 344 | ) 345 | {:mutations [] 346 | :sym->id {}} 347 | ast)] 348 | 349 | 350 | `(fn [~env-sym ~roots-sym ~nodes-sym ~oldv-sym ~newv-sym] 351 | ~@mutations))) 352 | 353 | (defn make-fragment [macro-env body] 354 | (let [env 355 | {:code-ref (atom {}) 356 | :el-seq-ref (atom -1) ;; want 0 to be first id 357 | ;; :parent (gensym "root") 358 | } 359 | 360 | ast 361 | (mapv #(analyze-node env %) body) 362 | 363 | code-snippets 364 | (->> @(:code-ref env) 365 | (sort-by val) 366 | (map (fn [[snippet id]] 367 | snippet)) 368 | (into [])) 369 | 370 | code-sym 371 | (gensym) 372 | 373 | ;; FIXME: use something that is guaranteed unique but shorter 374 | ;; can't use gensym because of caching (might cause conflicts when re-used) 375 | ;; it is useful to have the code location for debugging maybe but long string aren't that useful 376 | ;; in production build and just make everything bigger (not much but still) 377 | frag-id (str (-> macro-env :ns :name) "@" (:line macro-env) ":" (:column macro-env))] 378 | 379 | `(let [~code-sym (cljs.core/array ~@code-snippets) 380 | frag-id# ~frag-id] 381 | 382 | (or (fragment-get frag-id# ~code-sym) 383 | (fragment-reg frag-id# ~code-sym ~(make-build-impl ast) ~(make-update-impl ast))) 384 | ))) 385 | 386 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REPO OUT OF DATE! 2 | 3 | The current code lives in the [shadow-grove](https://github.com/thheller/shadow-grove) repo. 4 | 5 | The information below is somewhat outdated but the main concepts remain. 6 | 7 | ------------------- 8 | 9 | # shadow-arborist 10 | 11 | > Arborists generally focus on the health and safety of individual ... trees 12 | 13 | https://en.wikipedia.org/wiki/Arborist 14 | 15 | `shadow-arborist` is an experimental/research library exploring what a world without `react` would look like in CLJS. **This is not something you should build upon (yet).** 16 | 17 | ## Motivation 18 | 19 | The goal is to have a pure CLJS implementation for managing complex DOM trees and updating them in response to changing application state. `react` is widely adopted in the ClojureScript community and it is working reasonably well. There are however certain performance implications and more fundamentally we will always be constrained by what `react` offers us since `react` itself is not extensible. 20 | 21 | `react` first popularized the "Virtual DOM" where each actual DOM node is first represented by a virtual pure JS object representation that will be diffed to find the minimal set of changes necessary to update the actual DOM. 22 | 23 | Representing and diffing every single DOM node as a virtual node however isn't necessary if we can infer which nodes can actually change by doing a simple analysis of the code. The [svelte](https://svelte.dev/) framework wrote a completely custom compiler for this but in CLJS we can do this via macro. 24 | 25 | ## Architecture 26 | 27 | My current mental model does not go quite as far as `svelte` did and does keep some "virtual" representation of nodes. Each virtual Node however can represent one or more actual DOM nodes. 28 | 29 | Each "virtual" node can be turned into a "managed" node via a simple protocol implementation. The managed instances than becomes responsible for managing the actual DOM nodes and can take further "virtual" representations of itself to perform incremental updates. 30 | 31 | The basis for all this are a couple simple protocols: 32 | 33 | ```clojure 34 | (defprotocol IConstruct 35 | (as-managed [this env])) 36 | 37 | (defprotocol IManageNodes 38 | (dom-first [this]) 39 | (dom-insert [this parent anchor])) 40 | 41 | (defprotocol IUpdatable 42 | (supports? [this next]) 43 | (dom-sync! [this next])) 44 | 45 | (defprotocol IDestructible 46 | (destroy! [this])) 47 | ``` 48 | 49 | While these aren't final and will most likely change in some fashion they suffice to build a simple foundation that is easy to extend. 50 | 51 | While there will be many different implementations of these protocols we can start with its simplest implementation as an example first. 52 | 53 | The first "virtual" node will be a simple String: `"foo"`. The implementation for numbers and `nil` is actually the same so I'll just include them in this example. 54 | 55 | First the part that creates the "managed" version. 56 | 57 | ```clojure 58 | ;; see src/main/shadow/arborist/common.cljs 59 | 60 | (defn managed-text [env val] 61 | (ManagedText. env val (js/document.createTextNode (str val)))) 62 | 63 | (extend-protocol p/IConstruct 64 | string 65 | (as-managed [this env] 66 | (managed-text env this)) 67 | 68 | number 69 | (as-managed [this env] 70 | (managed-text env this)) 71 | 72 | ;; as a placeholder for (when condition (<< [:deep [:tree]])) 73 | nil 74 | (as-managed [this env] 75 | (managed-text env this))) 76 | ``` 77 | 78 | `env` is actually a CLJS persistent map which is similar to `Context` in `react`. It will just be passed down from the root to avoid relying on outside global state. This however is truly immutable and cannot be changed once the node has been created. Will cover this in more detail later ... 79 | 80 | So `"foo"` will be turned into a `ManagedText` instance which itself holds onto the `env` and the initial "virtual" node that create it. It also creates an actual DOM `#text` node directly which it is then responsible for. 81 | 82 | The creation of the node and adding it to the actual DOM tree is done in separate steps which will be invoked by the runtime at separate times. 83 | 84 | We can test this simply via a browser REPL (eg. `npx shadow-cljs browser-repl`). 85 | 86 | ```clojure 87 | (require '[shadow.arborist.protocols :as p]) 88 | (require '[shadow.arborist :as sa]) 89 | 90 | ;; not too important now 91 | (def env {}) 92 | 93 | ;; turn "foo" into a ManagedText instance 94 | (def node (p/as-managed "foo" env)) 95 | 96 | ;; insert it into the actual dom 97 | (p/dom-insert node js/document.body nil) 98 | ``` 99 | 100 | You can verify that the Browser REPL page now actually shows the "foo" text. 101 | 102 | The `dom-insert` method mirrors the DOM `parent.insertBefore(node, anchor)` [method](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore). With a `nil` `anchor` this will append as the last element instead. So effectively the `node` was just appended to the DOM. This is done via a protocol since the "managed" instance may want to add many nodes instead of just one and `.insertBefore` allows doing that. 103 | 104 | To simulate an update we first check if the managed node actually supports the new "virtual" node. 105 | 106 | ```clojure 107 | (p/supports? node "bar") ;; => true 108 | ``` 109 | 110 | Then to apply the actual update 111 | ```clojure 112 | (p/dom-sync! node "bar") 113 | ``` 114 | 115 | The actual document is now updated and `"foo"` was replaced by `"bar"`. 116 | 117 | If the `supports?` check fails the engine will instead create a new "managed" instance of the "virtual" node and then replace the old one in the actual DOM. 118 | 119 | ```clojure 120 | (require '[shadow.arborist.interpreted]) ;; basic hiccup support (actually optional) 121 | 122 | (def new-vnode [:h1 "hello world"]) 123 | 124 | (p/supports? node new-vnode) ;; => false 125 | 126 | (def new-node (p/as-managed new-vnode env)) 127 | ``` 128 | 129 | The `new-node` now exist but is not in the actual DOM yet. To get it into the correct position we need the `dom-first` method which gives us access to the first node the "managed" node is responsible for to serve as the `anchor` we need to insert. In this case it'll be the `#text` node we created earlier. 130 | 131 | ```clojure 132 | (def anchor (p/dom-first node)) 133 | 134 | ;; we need the parent node which we easily get via .-parentNode 135 | ;; actual implementations will often know the parentNode but this suffices 136 | (p/dom-insert new-node (.-parentNode anchor) anchor) 137 | ``` 138 | 139 | When looking at the browser both `new-node` and `node` are now actually in the DOM. To finish the actual replace we still need to remove the old version from the actual DOM which is covered by the `destroy!` protocol. 140 | 141 | ```clojure 142 | (p/destroy! node) 143 | ``` 144 | 145 | `destroy!` kind of implies that actually something desctructive will happen but actually the `node` will just remove all the nodes it is responsible for from the DOM. We could hold onto the managed instance and insert it into the DOM later or elsewhere. Most commonly we'll just forget about it and let it be garbage collected. 146 | 147 | We now have created one node and replaced it with another with only a very few basic ops. The actual "complicated" parts are nicely covered by separate protocol implementation that the basic algorithm doesn't need to know about. 148 | 149 | The `PersistentVector` implementation will actually only support updating nodes of the same time since we cannot turn an actual DOM `H1` node into a `DIV`. The replace logic will take care of this nicely though. 150 | 151 | ```clojure 152 | (p/supports? new-node [:div "foo?"]) ;; => false 153 | ``` 154 | 155 | With the basic "interpreted" hiccup-style vectors however we still are where we currently are with `react`. It is implemented in pure CLJS but we didn't gain anything new just yet. The whole benefit of this will only become clearer once we explore other implementations of the basic protocols. 156 | 157 | ## The "Fragment" Macro 158 | 159 | In DOM (or `react`) terms a fragment is a collection of zero or more DOM nodes. `react` supports this in JSX via the `<>` element and in Reagent this would be the `:<>` special keyword. I opted to use the `<<` symbol instead but the argument could be made to keep `<>`. 160 | 161 | ```clojure 162 | (require '[shadow.arborist :as sa :refer (<<)]) 163 | 164 | ;; just a basic example of how the update logic looks in actual code 165 | ;; the library actually covers this, just for explanation purposes 166 | (defn update-dom! [env old next] 167 | (if (p/supports? old next) 168 | (do (p/dom-sync! old next) 169 | old) 170 | (let [new (p/as-managed next env) 171 | anchor (p/dom-first old)] 172 | (p/dom-insert new (.-parentNode anchor) anchor) 173 | (p/destroy! old) 174 | new))) 175 | 176 | (def new-vnode (<< [:div.foo [:div.bar [:h1 "hello fragment"]]])) 177 | 178 | ;; update the dom with a variety of elements 179 | (def node (update-dom! env new-node new-vnode)) 180 | (def node (update-dom! env node [:h1 "foo"])) 181 | (def node (update-dom! env node new-vnode)) 182 | ``` 183 | 184 | So what is so different about the "fragment" macro? During macro expansion it can analyze the code and it will create 2 functions. One that will create the actual DOM nodes and one that will update them. In the example above all nodes are static so the update will just be a noop. 185 | 186 | So lets make things a bit more interesting: 187 | 188 | ```clojure 189 | (defn render [{:keys [title body]}] 190 | (<< [:div.card 191 | [:div.card-title title] 192 | [:div.card-body body] 193 | [:div.card-actions 194 | [:button "ok"]]])) 195 | 196 | (def node (update-dom! env node (render {:title "hello" :body "world"}))) 197 | (def node (update-dom! env node (render {:title "foo" :body "bar"}))) 198 | ``` 199 | 200 | Since a `symbol` is not a constant value the macro can actually extract those parts and the `render` fn will actually return a "virtual" fragment node which will contain a reference to the code for the actual fragment and a simple JS array holding all the state variables that can actually change. Pseudo-ish code looks something like 201 | 202 | ```clojure 203 | (defn render [{:keys [title body]}] 204 | (FragmentNode. (array title body) create-fn update-fn)) 205 | ``` 206 | 207 | The actual `create-fn` implementation might look a bit verbose but it just the minimal amount of steps to create all the actual DOM nodes. I assure you that after `:advanced` optmizations this will match hand-written manual DOM construction code. 208 | 209 | ```clojure 210 | (fn [env23497 vals23498] 211 | (let [el0_div (sf/create-element env23497 :div) 212 | el1_div (sf/create-element env23497 :div) 213 | d2 (sf/create-managed env23497 (aget vals23498 0)) 214 | el3_div (sf/create-element env23497 :div) 215 | d4 (sf/create-managed env23497 (aget vals23498 1)) 216 | el5_div (sf/create-element env23497 :div) 217 | el6_button (sf/create-element env23497 :button) 218 | G__23494 (sf/create-text env23497 "ok")] 219 | (sf/set-attr env23497 el0_div :class nil "card") 220 | (sf/append-child el0_div el1_div) 221 | (sf/set-attr env23497 el1_div :class nil "card-title") 222 | (sf/append-managed el1_div d2) 223 | (sf/append-child el0_div el3_div) 224 | (sf/set-attr env23497 el3_div :class nil "card-body") 225 | (sf/append-managed el3_div d4) 226 | (sf/append-child el0_div el5_div) 227 | (sf/set-attr env23497 el5_div :class nil "card-actions") 228 | (sf/append-child el5_div el6_button) 229 | (sf/append-child el6_button G__23494) 230 | (array 231 | (array el0_div) 232 | (array el0_div el1_div d2 el3_div d4 el5_div el6_button)))) 233 | ``` 234 | 235 | The `create-fn` takes two arguments: The `env` since it needs to be passed down to all other managed nodes and the `vals` array which contains `[title body]`. Since the same fragment will always produce the same array in the same order we'll just access the values by index. This could be a `PersistentVector` but since we are not never going to modify it using an `array` is fine and faster. 236 | 237 | The function will return 2 arrays. One for the actual root nodes that we'll need to add to the DOM later and one with all the nodes we are managing. This is an implementation detail and will most likely change. 238 | 239 | The `update-fn` will contain the minimal amount of steps required to get the data into the actual DOM. In this case something like 240 | 241 | ```clojure 242 | (fn [env roots nodes oldv newv] 243 | (sf/update-managed env roots nodes 2 (aget oldv 0) (aget newv 0)) 244 | (sf/update-managed env roots nodes 4 (aget oldv 1) (aget newv 1))) 245 | ``` 246 | 247 | It is purely for side-effects so the return value can be ignored. The arguments passed in will be 248 | 249 | - `env` as usual 250 | - `roots` the root nodes array created in `create-fn` 251 | - `nodes` the nodes array in `create-fn` 252 | - `oldv` the old values array, from the old virtual node 253 | - `newv` the new values array, from the new virtual node 254 | 255 | The implementation will first check if we are updating the `identical?` fragment and if not trigger the `replace` logic. 256 | 257 | In this case the implementation will update the actual live node at `nodes[2]` (and `4`) an update/replace it similar to the `update-dom!` logic. It will mutate the live DOM nodes and update the mutable array in place. No point in using persistent datastructures here since DOM is mutable anways. 258 | 259 | The implementation is likely to change a bit but it matches minimal code you would be writing by hand otherwise. We did not "diff" any of the `:div.*` elements since we know they can't change. So in all likelyhood we only update the `title` and `body` text assuming they changed. If not all we did is a few array lookups and compare two strings. 260 | 261 | I didn't do any real benchmarks yet but this should outperform `react` by quite a bit in cases where one fragment manages several nodes. Just look at some `svelte` benchmarks if you have any doubts. We'll be a bit slower than `svelte` probably but not by too much. 262 | 263 | The macro implementation is really basic and I wrote it in a weekend. We could probably do some very fun and sophisticated stuff there but the basic proof of concept already works quite nicely. 264 | 265 | ## Dealing with Collections 266 | 267 | Often we'll have collections of things that we'll want to display. In `react` there is only-one generic implementation for this is it is handled by having an array of React Element instances where each should have a `.key` property. This key property is used by the implementation to figure out if entries need to be re-ordered. Without a `.key` property the implementation will just render "over" the the elements which may result in many more DOM ops so it can be a lot less efficient than just re-ordering a few nodes. 268 | 269 | What always felt odd to me in `react` is that the User is responsible for setting the `.key` property on those React Element which will require iterating the collection which the implementation will then iterate over again. 270 | 271 | In `shadow-arborist` the control is flipped and instead the library will handle the `.key` extraction. All we need is the collection, a `key-fn` and a `render-fn` which will be applied to each item. 272 | 273 | 274 | ```clojure 275 | ;; simpler interop than update-dom! from above 276 | (def root (sa/dom-root (js/document.getElementById "app") env)) 277 | 278 | (defn render [items] 279 | (<< [:div "items:" (count items)] 280 | [:ul 281 | (sa/render-seq items identity 282 | (fn [item] 283 | (<< [:li "foo" item])))])) 284 | 285 | (p/update! root (render [1 2 3 4 5])) 286 | (p/update! root (render [3 5 1])) 287 | ``` 288 | 289 | The above example uses `identity` as the `key-fn` since the item is just a simple number and serves find as the key. In actual apps most commonly this would be a keyword and the items would be a sequential collection of maps. (eg. `[{:id 1 :text "foo"} ...]` with `:id` as `key-fn`). 290 | 291 | The current implementation is just a function call but that could be enhanced by a macro for some syntax sugar. 292 | 293 | The proof of concept implementation is quite simple and works well enough for demo purposes. It could be a bit more efficient and handle perserving active focused elements. Since this is built on the same simple protocols from above we could easily implement other implementations which are optimized for different use-cases. (eg. Drag-n-Drop sorted collections, append-only chat style). The implementations could also easily handle animations for moving items since they are much closer to the DOM and don't have to rebuild everything in the VDOM. 294 | 295 | 296 | ## Unshackled from React 297 | 298 | At this point we can do most of the things that `react` (and `react-dom`) actually do for us. The whole React "Fiber" architecture can be replicated using these simple protocols. This is the an area of active exploration and I haven't settled on anything yet. It works and is probably fast enough but I want to explore more. 299 | 300 | There are a lot of interesting topics to consider and ultimately the ideas behind React Fiber still apply even if the whole DOM aspect is faster. At some point we'll still want to skip some updates or delay them so higher priority updates can happen first. [Dan Abramov](https://twitter.com/dan_abramov) (React core dev) [tweeted](https://twitter.com/dan_abramov/status/1120971795425832961) a nice summary of why that architecture makes sense. This was somewhat in response to the [talk](https://www.youtube.com/watch?v=AdNJ3fydeao) given by [Rich Harris](https://twitter.com/rich_harris) (svelte author). I think the best approach is actually somewhere in the middle. 301 | 302 | `svelte` has some fantastic ideas like built-in transitions and `react` has some very nice ideas about more advanced scheduling (eg. delaying DOM updates to process user input faster, delaying low-priority offscreen DOM updates, etc). 303 | 304 | The goal of implementing all of this in CLJS is to make choices that make the most sense for CLJS. Instead of trying to make everything fit into the JS model of `react` we can save a whole bunch of work even if we just skip the translation steps. 305 | 306 | ## Things not mentioned yet ... 307 | 308 | There are a lot of other topics I didn't cover yet and will add later. 309 | 310 | - Components 311 | - Server-side Rendering using Clojure 312 | - `svelte` like transitions and other DOM stuff 313 | 314 | ## TBD 315 | 316 | Needless to say there is a lot of work to be done. I'm exploring this in my limited spare time so don't expect anything usable out of this anytime soon. I'm always looking for feedback and maybe you have some ideas I didn't think of. 317 | 318 | Please note that all of this is an experiment. The conclusion in the end may be to just use `react` but I so far I like the prospect of not having to. Since all of this is predicated on simple protocols we can also probably just swap out the macro an have something to spit out React elements in the end if needed. 319 | 320 | --------------------------------------------------------------------------------