├── .gitmodules ├── .gitignore ├── package.json ├── tests.edn ├── .dir-locals.el ├── src ├── deps.cljs ├── dumdom │ ├── dom │ │ └── uncontrolled.cljs │ ├── dom_macros.cljc │ ├── dom.cljc │ ├── core.clj │ ├── inflate.cljs │ ├── string.cljc │ ├── core.cljs │ ├── element.cljc │ └── component.cljc └── snabbdom │ ├── externs.js │ ├── snabbdom.min.js │ └── snabbdom.js ├── test └── dumdom │ ├── test_helper.cljs │ ├── test_runner.cljs │ ├── inflate_test.cljs │ ├── dom_test.clj │ ├── string_test.cljc │ ├── dom_test.cljs │ └── core_test.cljs ├── Makefile ├── resources └── public │ └── test-cards.html ├── dev.cljs.edn ├── deps.edn ├── pom.xml ├── dev └── dumdom │ └── dev.cljs ├── LICENSE └── Readme.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nrepl-port 2 | /figwheel_server.log 3 | node_modules 4 | resources/public/js 5 | target 6 | checkouts 7 | .cpcache 8 | cljs-test-runner-out 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "karma": "^3.1.4", 4 | "karma-chrome-launcher": "^2.2.0", 5 | "karma-cljs-test": "^0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :all 3 | :test-paths ["test"] 4 | :source-paths ["src"]}] 5 | :reporter [kaocha.report/documentation]} 6 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil 2 | (cider-clojure-cli-global-options . "-A:dev") 3 | (cider-default-cljs-repl . figwheel-main) 4 | (cider-figwheel-main-default-options . ":dev"))) 5 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:foreign-libs 2 | [{:file "snabbdom/snabbdom.js" 3 | :file-min "snabbdom/snabbdom.min.js" 4 | :provides ["snabbdom"]}] 5 | :externs ["snabbdom/externs.js"]} 6 | -------------------------------------------------------------------------------- /src/dumdom/dom/uncontrolled.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.dom.uncontrolled 2 | (:require [dumdom.dom :as d])) 3 | 4 | (defonce input d/input) 5 | (defonce textarea d/textarea) 6 | (defonce option d/option) 7 | -------------------------------------------------------------------------------- /test/dumdom/test_helper.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.test-helper 2 | (:require [dumdom.core :as dumdom])) 3 | 4 | (defn render 5 | ([el] 6 | (render el [] 0)) 7 | ([el path k] 8 | (el path k))) 9 | 10 | (defn render-str [& args] 11 | (apply dumdom/render-string args)) 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PATH := node_modules/.bin:$(PATH) 2 | 3 | node_modules/.bin/karma: 4 | npm install 5 | 6 | test: node_modules/.bin/karma 7 | clojure -A:dev -A:test 8 | clojure -A:dev -A:test-clj 9 | 10 | dumdom.jar: src/dumdom/* src/dumdom/dom/* 11 | rm -f dumdom.jar && clj -A:jar 12 | 13 | deploy: test dumdom.jar 14 | mvn deploy:deploy-file -Dfile=dumdom.jar -DrepositoryId=clojars -Durl=https://clojars.org/repo -DpomFile=pom.xml 15 | 16 | .PHONY: test deploy 17 | -------------------------------------------------------------------------------- /resources/public/test-cards.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dev.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:watch-dirs ["src" "test" "dev"] 2 | :extra-main-files {:tests {:main dumdom.test-runner}} 3 | :ring-server-options {:port 9595}} 4 | {:main "dumdom.dev" 5 | :optimizations :none 6 | :pretty-print true 7 | :source-map true 8 | :asset-path "/js/dev" 9 | :output-to "resources/public/js/dev.js" 10 | :output-dir "resources/public/js/dev" 11 | :closure-defines {cljs-test-display.core/notifications false 12 | cljs-test-display.core/printing false}} 13 | -------------------------------------------------------------------------------- /src/dumdom/dom_macros.cljc: -------------------------------------------------------------------------------- 1 | (ns dumdom.dom-macros) 2 | 3 | (defn- tag-definition 4 | "Return a form to define a wrapper function for a dumdom tag component" 5 | [tag] 6 | `(defn ~tag [& args#] 7 | (apply dumdom.dom/el ~(name tag) args#))) 8 | 9 | (defmacro define-tags 10 | "Macros which expands to a do block that defines top-level constructor functions 11 | for each supported HTML and SVG tag, using dumdom.dom/el" 12 | [& tags] 13 | `(do (do ~@(clojure.core/map tag-definition tags)) 14 | (def ~'defined-tags 15 | ~(zipmap tags 16 | (map (comp symbol name) tags))))) 17 | -------------------------------------------------------------------------------- /test/dumdom/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-hooks dumdom.test-runner 2 | (:require [cljs.test :as test] 3 | [cljs-test-display.core :as display] 4 | [dumdom.core-test] 5 | [dumdom.dom-test] 6 | [dumdom.inflate-test] 7 | [dumdom.string-test])) 8 | 9 | (enable-console-print!) 10 | 11 | (defn test-run [] 12 | (test/run-tests 13 | (display/init! "app-tests") 14 | 'dumdom.core-test 15 | 'dumdom.dom-test 16 | 'dumdom.string-test 17 | 'dumdom.inflate-test)) 18 | 19 | (defn ^:after-load render-on-relaod [] 20 | (test-run)) 21 | 22 | (test-run) 23 | -------------------------------------------------------------------------------- /test/dumdom/inflate_test.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.inflate-test 2 | (:require [cljs.test :refer-macros [deftest is testing]] 3 | [dumdom.dom :as d] 4 | [dumdom.inflate :as sut] 5 | [dumdom.string :as string])) 6 | 7 | (deftest inflate-rendering 8 | (testing "Does not remove existing valid DOM nodes" 9 | (let [el (js/document.createElement "div")] 10 | (set! (.-innerHTML el) "

Hello

") 11 | (set! (.. el -firstChild -marker) "marked") 12 | (sut/render (d/h1 {} "Hello") el) 13 | (is (= "marked" (.. el -firstChild -marker))))) 14 | 15 | (testing "Does not replace existing DOM nodes when elements have key" 16 | (let [el (js/document.createElement "div") 17 | component (d/h1 {:key "hello"} "Hello")] 18 | (set! (.-innerHTML el) (string/render component)) 19 | (set! (.. el -firstChild -marker) "marked") 20 | (sut/render (d/h1 {:key "hello"} "Hello") el) 21 | (is (= "marked" (.. el -firstChild -marker)))))) 22 | -------------------------------------------------------------------------------- /src/dumdom/dom.cljc: -------------------------------------------------------------------------------- 1 | (ns dumdom.dom 2 | (:require [dumdom.element :as element] 3 | #?(:clj [dumdom.dom-macros :as dm])) 4 | (:refer-clojure :exclude [time map meta mask]) 5 | #?(:cljs (:require-macros [dumdom.dom-macros :as dm]))) 6 | 7 | (defn el 8 | "Creates a virtual DOM element component of the specified type with attributes 9 | and optional children. Returns a function that renders the virtual DOM. This 10 | function expects a vector path and a key that addresses the component." 11 | [type attrs & children] 12 | (let [el-fn (apply element/create type attrs children)] 13 | #?(:cljs (set! (.-dumdom el-fn) true)) 14 | el-fn)) 15 | 16 | (dm/define-tags 17 | a abbr address area article aside audio b base bdi bdo big blockquote body br 18 | button canvas caption cite code col colgroup data datalist dd del details dfn 19 | div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 20 | head header hr html i iframe img input ins kbd keygen label legend li link main 21 | map mark menu menuitem meta meter nav noscript object ol optgroup option output 22 | p param pre progress q rp rt ruby s samp script section select small source 23 | span strong style sub summary sup table tbody td textarea tfoot th thead time 24 | title tr track u ul var video wbr circle clipPath defs ellipse g image line 25 | linearGradient mask path pattern polygon polyline radialGradient rect stop svg 26 | text tspan) 27 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.3"} 3 | org.clojure/clojurescript {:mvn/version "1.10.866"}} 4 | :aliases {:dev {:extra-paths ["dev" "test" "resources"] 5 | :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.2.13"} 6 | com.bhauman/cljs-test-display {:mvn/version "0.1.1"} 7 | devcards/devcards {:mvn/version "0.2.7"} 8 | cjohansen/quiescent {:mvn/version "16.2.0-3"}}} 9 | :repl {:extra-deps {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}} 10 | :main-opts ["-m" "figwheel.main" "-b" "dev" "-r"]} 11 | :dev-clj {:extra-paths ["dev" "test"]} 12 | :test {:extra-paths ["test" "resources/public"] 13 | :extra-deps {olical/cljs-test-runner {:mvn/version "3.3.0"}} 14 | :main-opts ["-m" "cljs-test-runner.main" "--env" "chrome-headless"]} 15 | :test-clj {:extra-paths ["test"] 16 | :extra-deps {lambdaisland/kaocha {:mvn/version "0.0-529"}} 17 | :main-opts ["-m" "kaocha.runner"]} 18 | :jar {:extra-deps {pack/pack.alpha {:git/url "https://github.com/juxt/pack.alpha.git" 19 | :sha "e518d9b2b70f4292c9988d2792b8667d88a6f4df"}} 20 | :main-opts ["-m" "mach.pack.alpha.skinny" "--no-libs" "--project-path" "dumdom.jar"]}}} 21 | -------------------------------------------------------------------------------- /src/dumdom/core.clj: -------------------------------------------------------------------------------- 1 | (ns dumdom.core 2 | (:require [dumdom.component :as component] 3 | [dumdom.string :as string])) 4 | 5 | (defn- extract-docstr 6 | [[docstr? & forms]] 7 | (if (string? docstr?) 8 | [docstr? forms] 9 | ["" (cons docstr? forms)])) 10 | 11 | (defn- extract-opts 12 | ([forms] (extract-opts forms {})) 13 | ([[k v & forms] opts] 14 | (if (keyword? k) 15 | (extract-opts forms (assoc opts k v)) 16 | [opts (concat [k v] forms)]))) 17 | 18 | (def component component/component) 19 | 20 | (defmacro defcomponent 21 | "Creates a component with the given name, a docstring (optional), any number of 22 | option->value pairs (optional), an argument vector and any number of forms 23 | body, which will be used as the rendering function to 24 | dumdom.core/component. 25 | 26 | For example: 27 | 28 | (defcomponent Widget 29 | \"A Widget\" 30 | :on-mount #(...) 31 | :on-render #(...) 32 | [value constant-value] 33 | (some-child-components)) 34 | 35 | Is shorthand for: 36 | 37 | (def Widget (dumdom.core/component 38 | (fn [value constant-value] (some-child-components)) 39 | {:on-mount #(...) 40 | :on-render #(...)}))" 41 | [name & forms] 42 | (let [[docstr forms] (extract-docstr forms) 43 | [options forms] (extract-opts forms) 44 | [argvec & body] forms 45 | options (merge {:name (str name)} options)] 46 | `(def ~name ~docstr (dumdom.core/component (fn ~argvec ~@body) ~options)))) 47 | 48 | (def render-string string/render) 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | cjohansen 5 | dumdom 6 | 2021.06.28-SNAPSHOT 7 | dumdom 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.0 13 | 14 | 15 | org.clojure 16 | clojurescript 17 | 1.10.439 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | clojars 26 | https://repo.clojars.org/ 27 | 28 | 29 | 30 | 31 | clojars 32 | Clojars repository 33 | https://clojars.org/repo 34 | 35 | 36 | Efficiently render and rerender DOM from immutable data. Like Quiescent, but without React. 37 | https://github.com/cjohansen/dumdom 38 | 39 | 40 | EPL 41 | https://opensource.org/licenses/EPL-2.0 42 | 43 | 44 | 45 | scm:git:git://github.com/cjohansen/dumdom.git 46 | scm:git:ssh://git@github.com/cjohansen/dumdom.git 47 | v2021.06.28-SNAPSHOT 48 | https://github.com/cjohansen/dumdom 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/dumdom/inflate.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.inflate 2 | (:require [cljs.reader :as reader] 3 | [dumdom.core :as dumdom])) 4 | 5 | (defn- init-node! [element] 6 | (dumdom/set-root-id element) 7 | (.-firstElementChild element)) 8 | 9 | (extend-type js/NodeList 10 | ISeqable 11 | (-seq [array] (array-seq array 0))) 12 | 13 | (defn- vnode [sel data children text elm] 14 | (cond-> {} 15 | (:key data) (assoc :key (reader/read-string (:key data))) 16 | sel (assoc :sel sel) 17 | data (assoc :data data) 18 | children (assoc :children children) 19 | text (assoc :text text) 20 | elm (assoc :elm elm))) 21 | 22 | (defn- props [node] 23 | (let [attributes (.-attributes node) 24 | len (.-length attributes)] 25 | (loop [props {} 26 | i 0] 27 | (if (< i len) 28 | (let [attr-name (.-nodeName (aget attributes i)) 29 | attr-val (.-nodeValue (aget attributes i))] 30 | (recur 31 | (if (= attr-name "data-dumdom-key") 32 | (assoc props :key (reader/read-string attr-val)) 33 | (assoc-in props [:attrs (keyword attr-name)] attr-val)) 34 | (inc i))) 35 | props)))) 36 | 37 | (declare to-vnode) 38 | 39 | (defn- element-vnode [node] 40 | (vnode (.toLowerCase (.-tagName node)) (props node) (map to-vnode (.-childNodes node)) nil node)) 41 | 42 | (defn- to-vnode [node] 43 | (cond 44 | (= 1 (.-nodeType node)) (element-vnode node) 45 | (= 3 (.-nodeType node)) (vnode nil nil nil (.-textContent node) node) 46 | (= 8 (.-nodeType node)) (vnode "!" {} [] (.-textContent node) node) 47 | :default (vnode "" {} [] nil node))) 48 | 49 | (defn render [component element] 50 | (let [current-node (or (dumdom/root-node element) (init-node! element)) 51 | element-id (.. element -dataset -dumdomId) 52 | vnode (clj->js (component [element-id] 0))] 53 | (dumdom/patch (clj->js (to-vnode current-node)) vnode) 54 | (dumdom/register-vnode element-id vnode))) 55 | -------------------------------------------------------------------------------- /src/snabbdom/externs.js: -------------------------------------------------------------------------------- 1 | var vdomNode = { 2 | /** @type {Array} */ 3 | children: [], 4 | data: { 5 | attrs: {}, 6 | hook: { 7 | destroy: function () {}, 8 | update: function () {}, 9 | insert: function () {}, 10 | remove: function () {}, 11 | pre: function () {} 12 | } 13 | }, 14 | elm: {}, 15 | key: {}, 16 | listener: function () {}, 17 | sel: "", 18 | text: "" 19 | }; 20 | 21 | /** 22 | * Externs for snabbdom. Previously generated with 23 | * http://jmmk.github.io/javascript-externs-generator, now compiled by hand. 24 | */ 25 | var snabbdom = { 26 | array: function () {}, 27 | attachTo: function () {}, 28 | attributesModule: { 29 | create: function () {}, 30 | update: function () {} 31 | }, 32 | classModule: { 33 | create: function () {}, 34 | update: function () {} 35 | }, 36 | datasetModule: { 37 | create: function () {}, 38 | update: function () {} 39 | }, 40 | eventListenersModule: { 41 | create: function () {}, 42 | update: function () {}, 43 | destroy: function () {} 44 | }, 45 | /** 46 | * @return {vdomNode} 47 | */ 48 | h: function () {}, 49 | htmlDomApi: { 50 | createElement: function () {}, 51 | createElementNS: function () {}, 52 | createTextNode: function () {}, 53 | createComment: function () {}, 54 | insertBefore: function () {}, 55 | removeChild: function () {}, 56 | appendChild: function () {}, 57 | parentNode: function () {}, 58 | nextSibling: function () {}, 59 | tagName: function () {}, 60 | setTextContent: function () {}, 61 | getTextContent: function () {}, 62 | isElement: function () {}, 63 | isText: function () {}, 64 | isComment: function () {} 65 | }, 66 | init: function () {}, 67 | jsx: function () {}, 68 | primitive: function () {}, 69 | propsModule: { 70 | create: function () {}, 71 | update: function () {} 72 | }, 73 | styleModule: { 74 | pre: function () {}, 75 | create: function () {}, 76 | update: function () {}, 77 | destroy: function () {}, 78 | remove: function () {} 79 | }, 80 | thunk: function () {}, 81 | /** 82 | * @return {vdomNode} 83 | */ 84 | toVNode: function () {}, 85 | vnode: function () {} 86 | }; 87 | -------------------------------------------------------------------------------- /src/dumdom/string.cljc: -------------------------------------------------------------------------------- 1 | (ns dumdom.string 2 | (:require [clojure.string :as str] 3 | [dumdom.element :as e])) 4 | 5 | (defn- tag-name [node] 6 | (:sel node)) 7 | 8 | (defn- children [node] 9 | (:children node)) 10 | 11 | (defn- attributes [node] 12 | (let [attrs (-> node :data :attrs)] 13 | (merge (some-> node :data :attrs) 14 | (some-> node :data :props) 15 | (some->> (-> node :data :dataset) 16 | (map (fn [[k v]] [(str "data-" (name k)) v])) 17 | (into {}))))) 18 | 19 | (defn- el-key [node] 20 | (:key node)) 21 | 22 | (defn- style [node] 23 | (-> node :data :style)) 24 | 25 | (defn- text-node? [vnode] 26 | (nil? (:sel vnode))) 27 | 28 | (defn- comment-node? [vnode] 29 | (= "!" (tag-name vnode))) 30 | 31 | (defn- text [vnode] 32 | (:text vnode)) 33 | 34 | (defn- kebab-case [s] 35 | (str/lower-case (str/replace s #"([a-z])([A-Z])" "$1-$2"))) 36 | 37 | (defn- render-styles [styles] 38 | (if (string? styles) 39 | styles 40 | (->> styles 41 | (remove (comp nil? second)) 42 | (map (fn [[k v]] (str (kebab-case (name k)) ": " v))) 43 | (str/join "; ")))) 44 | 45 | (defn- escape [s] 46 | (-> s 47 | (str/replace #"&(?!([a-z]+|#\d+);)" "&") 48 | (str/replace #"\"" """))) 49 | 50 | (defn- attrs [vnode] 51 | (let [k (el-key vnode) 52 | attributes (cond-> (dissoc (attributes vnode) :innerHTML) 53 | k (assoc :data-dumdom-key (escape (pr-str k)))) 54 | style (style vnode)] 55 | (->> (merge attributes 56 | (when style 57 | {:style (render-styles style)})) 58 | (map (fn [[k v]] (str " " (name k) "=\"" v "\""))) 59 | (str/join "")))) 60 | 61 | (def ^:private self-closing 62 | #{"area" "base" "br" "col" "embed" "hr" "img" "input" 63 | "link" "meta" "param" "source" "track" "wbr"}) 64 | 65 | (defn- closing-tag [tag-name] 66 | (when-not (self-closing tag-name) 67 | (str ""))) 68 | 69 | (defn- dom-str [vnode] 70 | (cond 71 | (or (nil? vnode) 72 | (comment-node? vnode)) "" 73 | (text-node? vnode) (text vnode) 74 | :default (str "<" (tag-name vnode) (attrs vnode) ">" 75 | (let [attrs (attributes vnode)] 76 | (if (contains? attrs :innerHTML) 77 | (:innerHTML attrs) 78 | (str/join "" (map dom-str (children vnode))))) 79 | (closing-tag (tag-name vnode))))) 80 | 81 | (defn render [component & [path k]] 82 | (let [component (e/inflate-hiccup component)] 83 | (dom-str (component (or path []) (or k 0))))) 84 | -------------------------------------------------------------------------------- /test/dumdom/dom_test.clj: -------------------------------------------------------------------------------- 1 | (ns dumdom.dom-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [dumdom.dom :as sut])) 4 | 5 | (defn remove-fns [x] 6 | (cond-> x 7 | (:data x) (update :data dissoc :on :hook))) 8 | 9 | (defn render [comp] 10 | (-> (comp [] 0) 11 | remove-fns 12 | (update :children #(map remove-fns %)))) 13 | 14 | (deftest element-test 15 | (testing "Renders element" 16 | (is (= {:sel "div" 17 | :data {:attrs {} 18 | :style nil 19 | :props {} 20 | :dataset {}} 21 | :children [{:text "Hello world"}]} 22 | (render (sut/div {} "Hello world"))))) 23 | 24 | (testing "Renders element with attributes, props, and styles" 25 | (is (= {:sel "input" 26 | :data {:attrs {:width 10} 27 | :style {:border "1px solid red"} 28 | :dataset {} 29 | :props {:value "Hello"}} 30 | :children [{:text "Hello world"}]} 31 | (render (sut/input {:width 10 32 | :value "Hello" 33 | :style {:border "1px solid red"}} 34 | "Hello world"))))) 35 | 36 | (testing "Renders element with children" 37 | (is (= {:sel "div" 38 | :data {:attrs {} 39 | :style nil 40 | :dataset {} 41 | :props {}} 42 | :children [{:sel "h1" 43 | :data {:style {:border "1px solid cyan"} 44 | :dataset {} 45 | :props {} 46 | :attrs {}} 47 | :children [{:text "Hello"}]} 48 | {:sel "img" 49 | :data {:style nil 50 | :attrs {:border "2"} 51 | :props {} 52 | :dataset {}} 53 | :children []}]} 54 | (render (sut/div {} 55 | (sut/h1 {:style {:border "1px solid cyan"}} "Hello") 56 | (sut/img {:border "2"})))))) 57 | 58 | (testing "Parses hiccup element name for classes" 59 | (is (= {:sel "div" 60 | :data {:attrs {} 61 | :style nil 62 | :dataset {} 63 | :props {}} 64 | :children [{:sel "h1" 65 | :data {:style nil 66 | :attrs {:class "something nice and beautiful"} 67 | :dataset {} 68 | :props {}} 69 | :children [{:text "Hello"}]}]} 70 | (render (sut/div {} [:h1.something.nice.and.beautiful "Hello"]))))) 71 | 72 | (testing "Parses hiccup element name for id and classes, combines with existing" 73 | (is (= {:sel "div" 74 | :data {:attrs {} 75 | :style nil 76 | :dataset {} 77 | :props {}} 78 | :children [{:sel "h1" 79 | :data {:style nil 80 | :attrs {:id "helau", :class "andhere here"} 81 | :dataset {} 82 | :props {}} 83 | :children [{:text "Hello"}]} 84 | {:sel "h1" 85 | :data {:style nil 86 | :attrs {:id "first", :class "lol"} 87 | :dataset {} 88 | :props {}} 89 | :children [{:text "Hello"}]}]} 90 | (render (sut/div {} 91 | [:h1.here#helau {:className "andhere"} "Hello"] 92 | [:h1#first.lol {} "Hello"])))))) 93 | -------------------------------------------------------------------------------- /dev/dumdom/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.dev 2 | (:require [dumdom.core :as dumdom :refer [defcomponent]] 3 | [dumdom.dom :as d] 4 | [snabbdom :as snabbdom])) 5 | 6 | (enable-console-print!) 7 | 8 | (def app (js/document.getElementById "app")) 9 | 10 | (defonce store (atom {:things [{:text "Thing 1" 11 | :id :t1} 12 | {:text "Thing 2" 13 | :id :t2} 14 | {:text "Thing 3" 15 | :id :t3}]})) 16 | 17 | (defn mark-active [things id] 18 | (mapv #(assoc % :active? (= (:id %) id)) things)) 19 | 20 | (defcomponent Thing 21 | :keyfn :id 22 | [{:keys [id idx active? text]}] 23 | [:div {:style {:cursor "pointer"} 24 | :key (name id) 25 | :onClick (fn [e] 26 | (swap! store update :things mark-active id))} 27 | (if active? 28 | [:strong text] 29 | text)]) 30 | 31 | (defcomponent App [data] 32 | [:div 33 | [:h1 "HELLO"] 34 | (map Thing (:things data))]) 35 | 36 | (defn render [state] 37 | (dumdom/render (App state) app)) 38 | 39 | (add-watch store :render (fn [_ _ _ state] 40 | (println "Render" state) 41 | (render state))) 42 | (render @store) 43 | 44 | (def patch (snabbdom/init #js [snabbdom/styleModule])) 45 | 46 | 47 | (comment 48 | (swap! store assoc :things []) 49 | 50 | (swap! store assoc :things [{:text "Thing 1" 51 | :id :t1} 52 | {:text "Thing 2" 53 | :id :t2} 54 | {:text "Thing 3" 55 | :id :t3}]) 56 | 57 | (swap! store assoc :things [{:text "Thing 1" 58 | :id :t1} 59 | {:text "Thing 2" 60 | :id :t2} 61 | {:text "Thing 3" 62 | :id :t3} 63 | {:text "Thing 4" 64 | :id :t4} 65 | {:text "Thing 5" 66 | :id :t5}]) 67 | 68 | (require '[quiescent.core :as q] 69 | '[quiescent.dom :as qd]) 70 | 71 | (dumdom/render [:div {} 72 | nil 73 | [:div "Dumdom"]] app) 74 | (dumdom/render [:div {} 75 | [:div {:style {:opacity 0.3 :transition "opacity 500ms"}} "Hello"] 76 | [:div "Dumdom"]] app) 77 | (dumdom/render [:div {} 78 | [:div {:style {:opacity 0.7 :transition "opacity 500ms"}} "Hello"] 79 | [:div "Dumdom"]] app) 80 | 81 | (def qel (js/document.createElement "div")) 82 | (js/document.body.appendChild qel) 83 | 84 | (q/render (qd/div {} 85 | nil 86 | (qd/div {} "Quiescent")) qel) 87 | (q/render (qd/div {} 88 | (qd/div {:style {:opacity 0.3 :transition "opacity 500ms"}} 89 | "Hello!") 90 | (qd/div {} "Quiescent")) qel) 91 | 92 | 93 | (def el (js/document.createElement "div")) 94 | (js/document.body.appendChild el) 95 | 96 | (js/console.log #js {:style #js {:opacity 0.3 :transition "opacity 500ms"}}) 97 | 98 | (def vdom (patch el (snabbdom/h "!" #js {} "nil"))) 99 | (def vdom (patch vdom (snabbdom/h "div" #js {} #js ["OK"]))) 100 | 101 | (def vdom (patch el (snabbdom/vnode "" #js {} #js []))) 102 | (def vdom (patch vdom (snabbdom/h "div" #js {} #js ["OK"]))) 103 | 104 | (def vdom (patch el (snabbdom/h "div" #js {} #js [(snabbdom/h "div" #js {} #js ["Hello from snabbdom"])]))) 105 | (def vdom (patch vdom (snabbdom/h 106 | "div" 107 | #js {} 108 | #js [(snabbdom/h 109 | "div" 110 | #js {:style #js {:opacity 0.3 :transition "opacity 500ms"}} 111 | #js ["Hello from snabbdom"])]))) 112 | 113 | (set! (.-innerHTML el) "Yo yoyo") 114 | (set! (.. el -style -transition) "opacity 0.5s") 115 | (set! (.. el -style -opacity) "0.3") 116 | ) 117 | -------------------------------------------------------------------------------- /src/dumdom/core.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.core 2 | (:require [dumdom.component :as component] 3 | [dumdom.dom :as d] 4 | [dumdom.element :as e] 5 | [dumdom.string :as string] 6 | [snabbdom :as snabbdom]) 7 | (:require-macros [dumdom.core])) 8 | 9 | (def ^:private current-nodes 10 | "A mapping from root DOM nodes to currently rendered virtual DOM trees. Used to 11 | reconcile (render component dom-node) to (patch old-vdom new-vdom)" 12 | (atom {})) 13 | 14 | (def ^:private element-id 15 | "A counter used to assign unique ids to root elements" 16 | (atom -1)) 17 | 18 | (def patch 19 | "The snabbdom patch function used by render" 20 | (snabbdom/init #js [snabbdom/eventListenersModule 21 | snabbdom/attributesModule 22 | snabbdom/propsModule 23 | snabbdom/styleModule 24 | snabbdom/datasetModule])) 25 | 26 | (defn set-root-id [element] 27 | (set! (.. element -dataset -dumdomId) (swap! element-id inc))) 28 | 29 | (defn root-node [element] 30 | (@current-nodes (.. element -dataset -dumdomId))) 31 | 32 | (defn register-vnode [element-id vnode] 33 | (swap! current-nodes assoc element-id vnode)) 34 | 35 | (defn unregister-vnode [element-id] 36 | (swap! current-nodes dissoc element-id)) 37 | 38 | (defn- init-node! 39 | "Snabbdom will replace the element provided as the original target for patch. 40 | When rendering into a new DOM node, we therefore create an intermediate in it 41 | and use that as Snabbdom's root, to avoid destroying the provided root node." 42 | [element] 43 | (set! (.-innerHTML element) "
") 44 | (set-root-id element) 45 | (.-firstElementChild element)) 46 | 47 | (defn purge! [] 48 | (reset! current-nodes {})) 49 | 50 | (defn render 51 | "Render the virtual DOM node created by the component into the specified DOM 52 | element, and mount it for fast future re-renders." 53 | [component element] 54 | (let [current-node (or (root-node element) (init-node! element)) 55 | element-id (.. element -dataset -dumdomId) 56 | component (e/inflate-hiccup component) 57 | vnode (component [element-id] 0)] 58 | (if vnode 59 | (let [vnode (clj->js vnode)] 60 | ;; If the root node does not have a key, Snabbdom will consider it the same 61 | ;; node as the node it is rendered into if they have the same tag name 62 | ;; (typically root nodes are divs, and typically they are rendered into 63 | ;; divs). When this happens, Snabbdom fires the update hook rather than the 64 | ;; insert hook, which breaks dumdom's contract. Forcing the root node to 65 | ;; have a key circumvents this problem and ensures the root node has its 66 | ;; insert hooks fired on initial render. 67 | (when-not (.. vnode -key) 68 | (set! (.. vnode -key) "root-node")) 69 | (patch current-node vnode) 70 | (register-vnode element-id vnode)) 71 | (do 72 | (set! (.-innerHTML element) "") 73 | (unregister-vnode element-id))) 74 | (when component/*render-eagerly?* 75 | (reset! component/eager-render-required? false)))) 76 | 77 | (defn render-once 78 | "Like render, but without mounting the element for future updates. This should 79 | only be used when you don't expect to re-render the component into the same 80 | element. Subsequent calls to render into the same element will always cause a 81 | full rebuild of the DOM. This function does not acumulate state." 82 | [component element] 83 | (let [current-node (init-node! element) 84 | component (e/inflate-hiccup component) 85 | vnode (component [element-id] 0)] 86 | (when-let [vnode (some-> (component [element-id] 0) clj->js)] 87 | (patch current-node vnode)) 88 | (when component/*render-eagerly?* 89 | (reset! component/eager-render-required? false)))) 90 | 91 | (defn unmount 92 | "Unmount an element previously mounted by dumdom.core/render" 93 | [element] 94 | (-> element .-dataset .-dumdomId unregister-vnode)) 95 | 96 | (def component component/component) 97 | (def component? component/component?) 98 | (def render-string string/render) 99 | 100 | (defn TransitionGroup [opt children] 101 | (component/TransitionGroup d/el opt children)) 102 | 103 | (defn CSSTransitionGroup [opt children] 104 | (component/CSSTransitionGroup d/el opt children)) 105 | -------------------------------------------------------------------------------- /test/dumdom/string_test.cljc: -------------------------------------------------------------------------------- 1 | (ns dumdom.string-test 2 | (:require #?(:clj [clojure.test :refer [deftest testing is]] 3 | :cljs [cljs.test :refer [deftest testing is]]) 4 | [dumdom.dom :as d] 5 | [dumdom.core :as dumdom])) 6 | 7 | (deftest render-test 8 | (testing "Renders element to string" 9 | (is (= "
Hello
" 10 | (dumdom/render-string (d/div {} "Hello"))))) 11 | 12 | (testing "Renders attributes to string" 13 | (is (= "
Hello
" 14 | (dumdom/render-string (d/div {:title "Title" :width 10} "Hello"))))) 15 | 16 | (testing "Renders self-closing tag" 17 | (is (= "" 18 | (dumdom/render-string (d/input {:width 20}))))) 19 | 20 | (testing "Renders self-closing tag with prop" 21 | (is (= "" 22 | (dumdom/render-string (d/input {:width 20 :value "Hello"}))))) 23 | 24 | (testing "Renders br" 25 | (is (= "
" 26 | (dumdom/render-string (d/br {}))))) 27 | 28 | (testing "Renders element with styles" 29 | (is (= "
Text
" 30 | (dumdom/render-string (d/div {:style {:border "1px solid red" 31 | :background "yellow"}} "Text"))))) 32 | 33 | (testing "Does not render nil styles" 34 | (is (= "
Text
" 35 | (dumdom/render-string (d/div {:style {:border nil 36 | :background "yellow"}} "Text"))))) 37 | 38 | (testing "Does not trip on nil children" 39 | (is (= "
Ok
" 40 | (dumdom/render-string (d/div {:style {:background "yellow"}} nil [:div "Ok"]))))) 41 | 42 | (testing "Renders element with properly cased styles" 43 | (is (= "
Text
" 44 | (dumdom/render-string (d/div {:style {:paddingLeft "10px"}} "Text"))))) 45 | 46 | (testing "Pixelizes style values" 47 | (is (= "
Text
" 48 | (dumdom/render-string (d/div {:style {:paddingLeft 10}} "Text"))))) 49 | 50 | (testing "Maps attribute names" 51 | (is (= "" 52 | (dumdom/render-string (d/label {:htmlFor "something"} "Text"))))) 53 | 54 | (testing "Renders element with children" 55 | (is (= "

Hello

World

" 56 | (dumdom/render-string (d/div {} 57 | (d/h1 {} "Hello") 58 | (d/p {} "World")))))) 59 | 60 | (testing "Renders custom component to string" 61 | (let [comp (dumdom/component (fn [data] (d/div {} (:text data))))] 62 | (is (= "
LOL
" 63 | (dumdom/render-string (comp {:text "LOL"})))))) 64 | 65 | (testing "Renders component key as data attribute" 66 | (is (= "
LOL
" 67 | (dumdom/render-string (d/div {:key "some-key"} "LOL"))))) 68 | 69 | (testing "Escapes ampersands in keys" 70 | (is (= "
LOL
" 71 | (dumdom/render-string (d/div {:key "some&key"} "LOL"))))) 72 | 73 | (testing "Respects existing HTML entities in keys" 74 | (is (= "
LOL
" 75 | (dumdom/render-string (d/div {:key "some"key"} "LOL"))))) 76 | 77 | (testing "Renders hiccup to string" 78 | (is (= "

Hello

World

" 79 | (dumdom/render-string [:div {:style {:color "red"}} 80 | [:h1 "Hello"] 81 | [:p "World"]])))) 82 | 83 | (testing "Renders block element with innerHTML" 84 | (is (= "

Hello

" 85 | (dumdom/render-string [:div {:dangerouslySetInnerHTML {:__html "

Hello

"}}])))) 86 | 87 | (testing "Renders inline element with innerHTML" 88 | (is (= "Hello" 89 | (dumdom/render-string [:span {:dangerouslySetInnerHTML {:__html "Hello"}}])))) 90 | 91 | (testing "Renders multiple child elements, strings and numbers alike" 92 | (is (= "These are 9 things" 93 | (dumdom/render-string [:span "These are " 9 " things"])))) 94 | 95 | (testing "Ignores event handlers" 96 | (is (= "Ok!" 97 | (dumdom/render-string [:a {:onClick (fn [_])} "Ok!"]))))) 98 | 99 | (deftest renders-string-styles 100 | (is (= "Ok" 101 | (dumdom/render-string [:a {:style "text-decoration:none;"} "Ok"])))) 102 | 103 | (deftest renders-nested-lists-and-lazy-seqs 104 | (is (= "
  • Text
  • " 105 | (dumdom/render-string 106 | [:li {} (list (map identity (list "Text")))])))) 107 | 108 | (deftest does-not-stop-on-nil-children 109 | (is (= "Click it" 110 | (dumdom/render-string 111 | [:a {:className "button text-m" :href "/"} 112 | nil 113 | "Click it"])))) 114 | 115 | (deftest renders-data-attributes 116 | (is (= "
    Content
    " 117 | (dumdom/render-string [:div {:data-stuff "Yes"} "Content"])))) 118 | -------------------------------------------------------------------------------- /test/dumdom/dom_test.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.dom-test 2 | (:require [cljs.test :as t :refer-macros [is testing]] 3 | [devcards.core :refer-macros [deftest]] 4 | [dumdom.core :as dd] 5 | [dumdom.dom :as d] 6 | [dumdom.test-helper :refer [render-str]])) 7 | 8 | (deftest dom-element-test 9 | (testing "Renders div element" 10 | (is (= "
    Hello
    " 11 | (render-str (d/div {:className "test"} "Hello"))))) 12 | 13 | (testing "Renders nested div elements" 14 | (is (= "
    Hello
    " 15 | (render-str (d/div {:className "test"} (d/div {:id "yap"} "Hello")))))) 16 | 17 | (testing "Renders CSS number values as pixel values" 18 | (is (= "
    Hello
    " 19 | (render-str (d/div {:style {:width 100 20 | :height 50 21 | :position "absolute" 22 | :left 10 23 | :top 20 24 | :right 30 25 | :bottom 40 26 | :padding 50 27 | :margin 20 28 | :opacity 0 29 | :flex 1}} "Hello"))))) 30 | 31 | (testing "Pixelizes snake-cased and camelCased CSS properties" 32 | (is (= "
    Hello
    " 33 | (render-str (d/div {:style {:margin-left 20 34 | :marginRight 20}} "Hello"))))) 35 | 36 | (testing "Supports dashed attribute names" 37 | (is (= "
    " 38 | (render-str (d/div {:class-name "hello"}))))) 39 | 40 | (testing "Purges nil attribute values" 41 | (is (= "" 42 | (render-str (d/img {:width 100 :height nil}))))) 43 | 44 | (testing "Renders SVG element" 45 | (let [el (js/document.createElement "div") 46 | brush {:fill "none" 47 | :strokeWidth "4" 48 | :strokeLinecap "round" 49 | :strokeLinejoin "round" 50 | :strokeMiterlimit "10"}] 51 | (dd/render (d/svg {:viewBox "0 0 64 64"} 52 | (d/circle (merge brush {:cx "32" :cy "32" :r "30"})) 53 | (d/line (merge brush {:x1 "16" :y1 "32" :x2 "48" :y2 "32"})) 54 | (d/line (merge brush {:x1 "32" :y1 "16" :x2 "32" :y2 "48"}))) el) 55 | (is (= (str "" 56 | "" 58 | "" 59 | "" 61 | "" 62 | "" 64 | "" 65 | "") 66 | (.-innerHTML el))))) 67 | 68 | (testing "Renders inner html" 69 | (is (= "

    Ok

    " 70 | (render-str (d/div {:dangerouslySetInnerHTML {:__html "

    Ok

    "}})))))) 71 | 72 | (deftest ref-test 73 | (testing "Invokes ref callback with DOM element" 74 | (let [node (atom nil) 75 | el (js/document.createElement "div")] 76 | (dd/render (d/div {:ref #(reset! node %)}) el) 77 | (is (= (.-firstChild el) @node)))) 78 | 79 | (testing "Does not invoke ref function on update" 80 | (let [calls (atom 0) 81 | el (js/document.createElement "div")] 82 | (dd/render (d/div {:ref #(swap! calls inc)} "Allo") el) 83 | (dd/render (d/div {:ref #(swap! calls inc)} "Allo!") el) 84 | (is (= 1 @calls)))) 85 | 86 | (testing "Invokes ref function with nil on unmount" 87 | (let [calls (atom []) 88 | el (js/document.createElement "div")] 89 | (dd/render (d/div {:ref #(swap! calls conj %)} "Allo") el) 90 | (dd/render (d/h1 {} "So long") el) 91 | (is (= 2 (count @calls))) 92 | (is (nil? (second @calls)))))) 93 | 94 | (deftest input-test 95 | (testing "Renders values in inputs" 96 | (let [el (js/document.createElement "div")] 97 | (dd/render (d/input {:value "Yowsa" :width "100" :type "text"}) el) 98 | (is (= "Yowsa" (.. el -firstChild -value))))) 99 | 100 | (testing "Does not render explicit 'null' for input value" 101 | (let [el (js/document.createElement "div")] 102 | (dd/render (d/input {:value nil}) el) 103 | (is (= "" (.. el -firstChild -value))))) 104 | 105 | (testing "Sets value for text areas" 106 | (let [el (js/document.createElement "div")] 107 | (dd/render (d/textarea {:value "Aloha"}) el) 108 | (is (= "Aloha" (.. el -firstChild -value))))) 109 | 110 | (testing "Does not render explicit 'null' for text areas" 111 | (let [el (js/document.createElement "div")] 112 | (dd/render (d/textarea {:value nil}) el) 113 | (is (= "" (.. el -firstChild -value)))))) 114 | 115 | (deftest hiccup-test 116 | (testing "Renders hiccup-like" 117 | (let [el (js/document.createElement "div")] 118 | (dd/render [:div {} "Hello world"] el) 119 | (is (= "
    Hello world
    " (.-innerHTML el))))) 120 | 121 | (testing "Renders hiccup-like with children" 122 | (let [el (js/document.createElement "div")] 123 | (dd/render [:div {} 124 | [:h1 {:style {:border "1px solid cyan"}} "Hello"] 125 | [:img {:border "2"}]] el) 126 | (is (= "

    Hello

    " (.. el -innerHTML))))) 127 | 128 | (testing "Renders mixed hiccup and functions" 129 | (let [el (js/document.createElement "div")] 130 | (dd/render [:div {} 131 | (d/h1 {:style {:border "1px solid cyan"}} [:a {} "Hello"])] el) 132 | (is (= "

    Hello

    " (.. el -innerHTML))))) 133 | 134 | (testing "Accepts omission of attribute map in hiccup syntax" 135 | (let [el (js/document.createElement "div")] 136 | (dd/render [:div 137 | [:h1 "Hello"] 138 | [:img {:border "2"}]] el) 139 | (is (= "

    Hello

    " (.. el -innerHTML))))) 140 | 141 | (testing "Parses hiccup element name for classes" 142 | (let [el (js/document.createElement "div")] 143 | (dd/render [:div.something.nice.and.beautiful "Hello"] el) 144 | (is (= "
    Hello
    " (.. el -innerHTML))))) 145 | 146 | (testing "Combines hiccup symbol classes with attribute classes" 147 | (let [el (js/document.createElement "div")] 148 | (dd/render [:div.something {:className "nice"} "Hello"] el) 149 | (is (= "
    Hello
    " (.. el -innerHTML))))) 150 | 151 | (testing "Sets inner HTML" 152 | (let [el (js/document.createElement "div")] 153 | (dd/render [:div {:dangerouslySetInnerHTML {:__html "

    LOL!

    "}}] el) 154 | (is (= "

    LOL!

    " (.. el -innerHTML))))) 155 | 156 | (testing "Clears inner HTML" 157 | (let [el (js/document.createElement "div")] 158 | (dd/render [:div {:dangerouslySetInnerHTML {:__html "

    LOL!

    "}}] el) 159 | (dd/render [:div {:dangerouslySetInnerHTML {:__html nil}}] el) 160 | (is (= "
    " (.. el -innerHTML)))))) 161 | 162 | (deftest data-attributes-test 163 | (testing "Renders data attributes" 164 | (let [el (js/document.createElement "div")] 165 | (dd/render [:div {:data-stuff "32" :data-other 12} "Hello"] el) 166 | (is (= "
    Hello
    " (.. el -innerHTML)))))) 167 | -------------------------------------------------------------------------------- /src/dumdom/element.cljc: -------------------------------------------------------------------------------- 1 | (ns dumdom.element 2 | (:require [clojure.set :as set] 3 | [clojure.string :as str])) 4 | 5 | (defn- event-entry [attrs k] 6 | [(.toLowerCase (.substring (name k) 2)) (attrs k)]) 7 | 8 | (defn- camelCase [s] 9 | (let [[f & rest] (str/split s #"-")] 10 | (str f (str/join "" (map str/capitalize rest))))) 11 | 12 | (defn- camel-key [k] 13 | (keyword (camelCase (name k)))) 14 | 15 | (def ^:private skip-pixelize-attrs 16 | (->> 17 | [:animation-iteration-count 18 | :box-flex 19 | :box-flex-group 20 | :box-ordinal-group 21 | :column-count 22 | :fill-opacity 23 | :flex 24 | :flex-grow 25 | :flex-positive 26 | :flex-shrink 27 | :flex-negative 28 | :flex-order 29 | :font-weight 30 | :line-clamp 31 | :line-height 32 | :opacity 33 | :order 34 | :orphans 35 | :stop-opacity 36 | :stroke-dashoffset 37 | :stroke-opacity 38 | :stroke-width 39 | :tab-size 40 | :widows 41 | :z-index 42 | :zoom] 43 | (mapcat (fn [k] [k (camel-key k)])) 44 | set)) 45 | 46 | (defn- normalize-styles [styles] 47 | (reduce (fn [m [attr v]] 48 | (if (number? v) 49 | (if (skip-pixelize-attrs attr) 50 | (update m attr str) 51 | (update m attr str "px")) 52 | m)) 53 | styles 54 | styles)) 55 | 56 | (def ^:private attr-mappings 57 | {:acceptCharset :accept-charset 58 | :accessKey :accesskey 59 | :autoCapitalize :autocapitalize 60 | :autoComplete :autocomplete 61 | :autoFocus :autofocus 62 | :autoPlay :autoplay 63 | :bgColor :bgcolor 64 | :className :class 65 | :codeBase :codebase 66 | :colSpan :colspan 67 | :contentEditable :contenteditable 68 | :contextMenu :contextmenu 69 | :crossOrigin :crossorigin 70 | :dateTime :datetime 71 | :dirName :dirname 72 | :dropZone :dropzone 73 | :encType :enctype 74 | :htmlFor :for 75 | :formAction :formaction 76 | :hrefLang :hreflang 77 | :httpEquiv :http-equiv 78 | :isMap :ismap 79 | :itemProp :itemprop 80 | :keyType :keytype 81 | :maxLength :maxlength 82 | :minLength :minlength 83 | :noValidate :novalidate 84 | :placeHolder :placeholder 85 | :preLoad :preload 86 | :radioGroup :radiogroup 87 | :readOnly :readonly 88 | :rowSpan :rowspan 89 | :spellCheck :spellcheck 90 | :srcDoc :srcdoc 91 | :srcLang :srclang 92 | :srcSet :srcset 93 | :tabIndex :tabindex 94 | :useMap :usemap 95 | :accentHeight :accent-height 96 | :alignmentBaseline :alignment-baseline 97 | :arabicForm :arabic-form 98 | :baselineShift :baseline-shift 99 | :capHeight :cap-height 100 | :clipPath :clip-path 101 | :clipRule :clip-rule 102 | :colorInterpolation :color-interpolation 103 | :colorInterpolationFilters :color-interpolation-filters 104 | :colorProfile :color-profile 105 | :colorRendering :color-rendering 106 | :dominantBaseline :dominant-baseline 107 | :enableBackground :enable-background 108 | :fillOpacity :fill-opacity 109 | :fillRule :fill-rule 110 | :floodColor :flood-color 111 | :floodOpacity :flood-opacity 112 | :fontFamily :font-family 113 | :fontSize :font-size 114 | :fontSizeAdjust :font-size-adjust 115 | :fontStretch :font-stretch 116 | :fontStyle :font-style 117 | :fontVariant :font-variant 118 | :fontWeight :font-weight 119 | :glyphName :glyph-name 120 | :glyphOrientationHorizontal :glyph-orientation-horizontal 121 | :glyphOrientationVertical :glyph-orientation-vertical 122 | :horizAdvX :horiz-adv-x 123 | :horizOriginX :horiz-origin-x 124 | :imageRendering :image-rendering 125 | :letterSpacing :letter-spacing 126 | :lightingColor :lighting-color 127 | :markerEnd :marker-end 128 | :markerMid :marker-mid 129 | :markerStart :marker-start 130 | :overlinePosition :overline-position 131 | :overlineThickness :overline-thickness 132 | :panose1 :panose-1 133 | :paintOrder :paint-order 134 | :pointerEvents :pointer-events 135 | :renderingIntent :rendering-intent 136 | :shapeRendering :shape-rendering 137 | :stopColor :stop-color 138 | :stopOpacity :stop-opacity 139 | :strikethroughPosition :strikethrough-position 140 | :strikethroughThickness :strikethrough-thickness 141 | :strokeDasharray :stroke-dasharray 142 | :strokeDashoffset :stroke-dashoffset 143 | :strokeLinecap :stroke-linecap 144 | :strokeLinejoin :stroke-linejoin 145 | :strokeMiterlimit :stroke-miterlimit 146 | :strokeOpacity :stroke-opacity 147 | :strokeWidth :stroke-width 148 | :textAnchor :text-anchor 149 | :textDecoration :text-decoration 150 | :textRendering :text-rendering 151 | :underlinePosition :underline-position 152 | :underlineThickness :underline-thickness 153 | :unicodeBidi :unicode-bidi 154 | :unicodeRange :unicode-range 155 | :unitsPerEm :units-per-em 156 | :vAlphabetic :v-alphabetic 157 | :vHanging :v-hanging 158 | :vIdeographic :v-ideographic 159 | :vMathematical :v-mathematical 160 | :vectorEffect :vector-effect 161 | :vertAdvY :vert-adv-y 162 | :vertOriginX :vert-origin-x 163 | :vertOriginY :vert-origin-y 164 | :wordSpacing :word-spacing 165 | :writingMode :writing-mode 166 | :xHeight :x-height 167 | :xlinkActuate :xlink:actuate 168 | :xlinkArcrole :xlink:arcrole 169 | :xlinkHref :xlink:href 170 | :xlinkRole :xlink:role 171 | :xlinkShow :xlink:show 172 | :xlinkTitle :xlink:title 173 | :xlinkType :xlink:type 174 | :xmlBase :xml:base 175 | :xmlLang :xml:lang 176 | :xmlSpace :xml:space 177 | :mountedStyle :mounted-style 178 | :leavingStyle :leaving-style 179 | :disappearingStyle :disappearing-style}) 180 | 181 | (defn data-attr? [[k v]] 182 | (re-find #"^data-" (name k))) 183 | 184 | (defn- prep-attrs [attrs k] 185 | (let [event-keys (filter #(and (str/starts-with? (name %) "on") (ifn? (attrs %))) (keys attrs)) 186 | dataset (->> attrs 187 | (filter data-attr?) 188 | (map (fn [[k v]] [(str/replace (name k) #"^data-" "") v])) 189 | (into {})) 190 | attrs (->> attrs 191 | (remove data-attr?) 192 | (map (fn [[k v]] [(camel-key k) v])) 193 | (remove (fn [[k v]] (nil? v))) 194 | (into {})) 195 | attrs (set/rename-keys attrs attr-mappings) 196 | el-key (or (:key attrs) 197 | (when (contains? attrs :dangerouslySetInnerHTML) 198 | (hash [(:dangerouslySetInnerHTML attrs) k])))] 199 | (cond-> {:attrs (apply dissoc attrs :style :mounted-style :leaving-style :disappearing-style 200 | :component :value :key :ref :dangerouslySetInnerHTML event-keys) 201 | :props (cond-> {} 202 | (:value attrs) (assoc :value (:value attrs)) 203 | 204 | (contains? (:dangerouslySetInnerHTML attrs) :__html) 205 | (assoc :innerHTML (-> attrs :dangerouslySetInnerHTML :__html))) 206 | :style (merge (normalize-styles (:style attrs)) 207 | (when-let [enter (:mounted-style attrs)] 208 | {:delayed (normalize-styles enter)}) 209 | (when-let [remove (:leaving-style attrs)] 210 | {:remove (normalize-styles remove)}) 211 | (when-let [destroy (:disappearing-style attrs)] 212 | {:destroy (normalize-styles destroy)})) 213 | :on (->> event-keys 214 | (mapv #(event-entry attrs %)) 215 | (into {})) 216 | :hook (merge 217 | {} 218 | (when-let [callback (:ref attrs)] 219 | {:insert #(callback (.-elm %)) 220 | :destroy #(callback nil)})) 221 | :dataset dataset} 222 | el-key (assoc :key el-key)))) 223 | 224 | (declare create) 225 | 226 | (defn hiccup? [sexp] 227 | (and (vector? sexp) 228 | (not (map-entry? sexp)) 229 | (or (keyword? (first sexp)) (fn? (first sexp))))) 230 | 231 | (defn parse-hiccup-symbol [sym attrs] 232 | (let [[_ id] (re-find #"#([^\.#]+)" sym) 233 | [el & classes] (-> (str/replace sym #"#([^#\.]+)" "") 234 | (str/split #"\."))] 235 | [el 236 | (cond-> attrs 237 | id (assoc :id id) 238 | (seq classes) (update :className #(str/join " " (if % (conj classes %) classes))))])) 239 | 240 | (defn explode-styles [s] 241 | (->> (str/split s #";") 242 | (map #(let [[k v] (map str/trim (str/split % #":"))] 243 | [k v])) 244 | (into {}))) 245 | 246 | (defn prep-hiccup-attrs [attrs] 247 | (cond-> attrs 248 | (string? (:style attrs)) (update :style explode-styles))) 249 | 250 | (defn flatten-seqs [xs] 251 | (loop [res [] 252 | [x & xs] xs] 253 | (cond 254 | (and (nil? xs) (nil? x)) (seq res) 255 | (seq? x) (recur (into res (flatten-seqs x)) xs) 256 | :default (recur (conj res x) xs)))) 257 | 258 | (defn add-namespace [vnode] 259 | (cond-> vnode 260 | (not= "foreignObject" (:sel vnode)) 261 | (assoc-in [:data :ns] "http://www.w3.org/2000/svg") 262 | 263 | (:children vnode) 264 | (update :children #(map add-namespace %)))) 265 | 266 | (defn svg? [sel] 267 | (and (= "s" (nth sel 0)) 268 | (= "v" (nth sel 1)) 269 | (= "g" (nth sel 2)) 270 | (or (= 3 (count sel)) 271 | (= "." (nth sel 3)) 272 | (= "#" (nth sel 3))))) 273 | 274 | (defn primitive? [x] 275 | (or (string? x) (number? x))) 276 | 277 | (defn convert-primitive-children [children] 278 | (for [c children] 279 | (if (primitive? c) 280 | {:text c} 281 | c))) 282 | 283 | ;; This is a port of Snabbdom's `h` function, but without the varargs support. 284 | (defn create-vdom-node [sel attrs children] 285 | (let [cmap? (map? children)] 286 | (cond-> {:sel sel 287 | :data (dissoc attrs :key)} 288 | (primitive? children) 289 | (assoc :text children) 290 | 291 | cmap? 292 | (assoc :children [children]) 293 | 294 | (and (seq? children) (not cmap?)) 295 | (assoc :children children) 296 | 297 | :always (update :children convert-primitive-children) 298 | 299 | (svg? sel) 300 | add-namespace 301 | 302 | (:key attrs) 303 | (assoc :key (:key attrs))))) 304 | 305 | (defn inflate-hiccup [sexp] 306 | (cond 307 | (nil? sexp) (create-vdom-node "!" {} "nil") 308 | 309 | (not (hiccup? sexp)) sexp 310 | 311 | :default 312 | (let [tag-name (first sexp) 313 | args (rest sexp) 314 | args (if (map? (first args)) args (concat [{}] args))] 315 | (if (fn? tag-name) 316 | (apply tag-name (rest sexp)) 317 | (let [[element attrs] (parse-hiccup-symbol (name tag-name) (first args))] 318 | (apply create element (prep-hiccup-attrs attrs) (flatten-seqs (rest args)))))))) 319 | 320 | (defn create [tag-name attrs & children] 321 | (fn [path k] 322 | (let [fullpath (conj path k)] 323 | (create-vdom-node 324 | tag-name 325 | (-> (prep-attrs attrs k) 326 | (assoc-in [:hook :update] 327 | (fn [old-vnode new-vnode] 328 | (doseq [node (filter #(some-> % .-willEnter) (.-children new-vnode))] 329 | ((.-willEnter node))) 330 | (doseq [node (filter #(some-> % .-willAppear) (.-children new-vnode))] 331 | ((.-willAppear node)))))) 332 | (->> children 333 | (mapcat #(if (seq? %) % [%])) 334 | (map inflate-hiccup) 335 | (map-indexed #(do 336 | (if (fn? %2) 337 | (%2 fullpath %1) 338 | %2)))))))) 339 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/snabbdom/snabbdom.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var snabbdom={};function createElement(e,t){return document.createElement(e,t)}function createElementNS(e,t,n){return document.createElementNS(e,t,n)}function createTextNode(e){return document.createTextNode(e)}function createComment(e){return document.createComment(e)}function insertBefore(e,t,n){e.insertBefore(t,n)}function removeChild(e,t){t.parentNode&&t.parentNode.removeChild(t)}function appendChild(e,t){e.appendChild(t)}function parentNode(e){return e.parentNode}function nextSibling(e){return e.nextSibling}function tagName(e){return e.tagName}function setTextContent(e,t){e.textContent=t}function getTextContent(e){return e.textContent}function isElement(e){return 1===e.nodeType}function isText(e){return 3===e.nodeType}function isComment(e){return 8===e.nodeType}const htmlDomApi={createElement:createElement,createElementNS:createElementNS,createTextNode:createTextNode,createComment:createComment,insertBefore:insertBefore,removeChild:removeChild,appendChild:appendChild,parentNode:parentNode,nextSibling:nextSibling,tagName:tagName,setTextContent:setTextContent,getTextContent:getTextContent,isElement:isElement,isText:isText,isComment:isComment};function vnode(e,t,n,o,a){return{sel:e,data:t,children:n,text:o,elm:a,key:void 0===t?void 0:t.key}}const array=Array.isArray;function primitive(e){return"string"==typeof e||"number"==typeof e}function isUndef(e){return void 0===e}function isDef(e){return void 0!==e}const emptyNode=vnode("",{},[],void 0,void 0);function sameVnode(e,t){var n=e.key===t.key,o=(null===(o=e.data)||void 0===o?void 0:o.is)===(null===(o=t.data)||void 0===o?void 0:o.is);return e.sel===t.sel&&n&&o}function isVnode(e){return void 0!==e.sel}function createKeyToOldIdx(t,n,o){const a={};for(let e=n;e<=o;++e){var r=null===(r=t[e])||void 0===r?void 0:r.key;void 0!==r&&(a[r]=e)}return a}const hooks=["create","update","remove","destroy","pre","post"];function init$1(e,t){let n,o;const v={create:[],update:[],remove:[],destroy:[],pre:[],post:[]},p=void 0!==t?t:htmlDomApi;for(n=0;ni?y(e,null==n[s+1]?null:n[s+1].elm,n,r,s,o):g(e,t,a,i))}(i,l,d,e):isDef(d)?(isDef(t.text)&&p.setTextContent(i,""),y(i,null,d,0,d.length-1,e)):isDef(l)?g(i,l,0,l.length-1):isDef(t.text)&&p.setTextContent(i,""):t.text!==n.text&&(isDef(l)&&g(i,l,0,l.length-1),p.setTextContent(i,n.text)),null!==(i=null==r?void 0:r.postpatch)&&void 0!==i&&i.call(r,t,n)}}return function(e,t){let n,o,a;const r=[];for(n=0;n rendered 25 | k (assoc :key (cond 26 | (or (string? k) 27 | (number? k)) k 28 | (keyword? k) (str k) 29 | :default (hash k)))))) 30 | 31 | (defn setup-animation-hooks [rendered animation {:keys [will-enter will-appear]}] 32 | (when will-appear 33 | (swap! animation assoc :will-appear will-appear)) 34 | (cond-> rendered 35 | will-enter (assoc :willEnter #(swap! animation assoc :will-enter will-enter)) 36 | will-appear (assoc :willAppear #(swap! animation dissoc :will-appear)))) 37 | 38 | (defn- setup-mount-hook [rendered {:keys [on-mount on-render will-appear did-appear will-enter did-enter]} data args animation] 39 | (cond-> rendered 40 | (or on-mount on-render will-enter will-appear) 41 | (update-in 42 | [:data :hook :insert] 43 | (fn [insert-hook] 44 | (fn [vnode] 45 | (when insert-hook (insert-hook vnode)) 46 | (when on-mount (apply on-mount (.-elm vnode) data args)) 47 | (when on-render (apply on-render (.-elm vnode) data nil args)) 48 | (let [{:keys [will-enter will-appear]} @animation] 49 | (when-let [callback (or will-enter will-appear)] 50 | (swap! animation assoc :ready? false) 51 | (apply callback 52 | (.-elm vnode) 53 | (fn [] 54 | (swap! animation assoc :ready? true) 55 | (when-let [completion (if (= callback will-enter) 56 | did-enter 57 | did-appear)] 58 | (apply completion (.-elm vnode) data args))) 59 | data 60 | args)))))))) 61 | 62 | (defn- setup-update-hook [rendered {:keys [on-update on-render]} data old-data args] 63 | (cond-> rendered 64 | (or on-update on-render) 65 | (assoc-in 66 | [:data :hook :update] 67 | (fn [old-vnode vnode] 68 | (when on-update (apply on-update (.-elm vnode) data old-data args)) 69 | (when on-render (apply on-render (.-elm vnode) data old-data args)))))) 70 | 71 | (defn- setup-unmount-hook [rendered component data args animation on-destroy] 72 | (cond-> rendered 73 | :always 74 | (assoc-in 75 | [:data :hook :destroy] 76 | (fn [vnode] 77 | (when-let [on-unmount (:on-unmount component)] 78 | (apply on-unmount (.-elm vnode) data args)) 79 | (on-destroy))) 80 | 81 | (:will-leave component) 82 | (assoc-in 83 | [:data :hook :remove] 84 | (fn [vnode snabbdom-callback] 85 | (let [callback (fn [] 86 | (when-let [did-leave (:did-leave component)] 87 | (apply did-leave (.-elm vnode) data args)) 88 | (snabbdom-callback))] 89 | (if (:ready? @animation) 90 | (apply (:will-leave component) (.-elm vnode) callback data args) 91 | (add-watch animation :leave 92 | (fn [k r o n] 93 | (when (:ready? n) 94 | (remove-watch animation :leave) 95 | (apply (:will-leave component) (.-elm vnode) callback data args)))))))))) 96 | 97 | (defn component 98 | "Returns a component function that uses the provided function for rendering. The 99 | resulting component will only call through to its rendering function when 100 | called with data that is different from the data that produced the currently 101 | rendered version of the component. 102 | 103 | The rendering function can be called with any number of arguments, but only 104 | the first one will influence rendering decisions. You should call the 105 | component with a single immutable value, followed by any number of other 106 | arguments, as desired. These additional constant arguments are suitable for 107 | passing messaging channels, configuration maps, and other utilities that are 108 | constant for the lifetime of the rendered element. 109 | 110 | The optional opts argument is a map with additional properties: 111 | 112 | :on-mount - A function invoked once, immediately after initial rendering. It 113 | is passed the rendered DOM node, and all arguments passed to the render 114 | function. 115 | 116 | :on-update - A function invoked immediately after an updated is flushed to the 117 | DOM, but not on the initial render. It is passed the underlying DOM node, the 118 | value, and any constant arguments passed to the render function. 119 | 120 | :on-render - A function invoked immediately after the DOM is updated, both on 121 | the initial render and subsequent updates. It is passed the underlying DOM 122 | node, the value, the old value, and any constant arguments passed to the 123 | render function. 124 | 125 | :on-unmount - A function invoked immediately before the component is unmounted 126 | from the DOM. It is passed the underlying DOM node, the most recent value and 127 | the most recent constant args passed to the render fn. 128 | 129 | :will-appear - A function invoked when this component is added to a mounting 130 | container component. Invoked at the same time as :on-mount. It is passed the 131 | underlying DOM node, a callback function, the most recent value and the most 132 | recent constant args passed to the render fn. The callback should be called to 133 | indicate that the element is done \"appearing\". 134 | 135 | :did-appear - A function invoked immediately after the callback passed 136 | to :will-appear is called. It is passed the underlying DOM node, the most 137 | recent value, and the most recent constant args passed to the render fn. 138 | 139 | :will-enter - A function invoked when this component is added to an already 140 | mounted container component. Invoked at the same time as :on.mount. It is 141 | passed the underlying DOM node, a callback function, the value and any 142 | constant args passed to the render fn. The callback function should be called 143 | to indicate that the element is done entering. 144 | 145 | :did-enter - A function invoked after the callback passed to :will-enter is 146 | called. It is passed the underlying DOM node, the value and any constant args 147 | passed to the render fn. 148 | 149 | :will-leave - A function invoked when this component is removed from its 150 | containing component. Is passed the underlying DOM node, a callback function, 151 | the most recent value and the most recent constant args passed to the render 152 | fn. The DOM node will not be removed until the callback is called. 153 | 154 | :did-leave - A function invoked after the callback passed to :will-leave is 155 | called (at the same time as :on-unmount). Is passed the underlying DOM node, 156 | the most recent value and the most recent constant args passed to the render 157 | fn." 158 | ([render] (component render {})) 159 | ([render opt] 160 | (when *render-eagerly?* 161 | (reset! eager-render-required? true)) 162 | (let [instances (atom {})] 163 | (fn [data & args] 164 | (let [comp-fn 165 | (fn [path k] 166 | (let [key (when-let [keyfn (:keyfn opt)] (keyfn data)) 167 | fullpath (conj path (or key k)) 168 | instance (@instances fullpath) 169 | animation (atom {:ready? true})] 170 | (if (should-component-update? instance data) 171 | (let [rendered 172 | (some-> 173 | (when-let [vdom (apply render data args)] 174 | ((e/inflate-hiccup vdom) fullpath 0)) 175 | #?(:cljs (set-key key)) 176 | #?(:cljs (setup-animation-hooks animation opt)) 177 | #?(:cljs (setup-unmount-hook opt data args animation #(swap! instances dissoc fullpath))))] 178 | (swap! instances assoc fullpath {:vdom rendered :data data}) 179 | ;; The insert and update hooks are added after the instance 180 | ;; is cached. When used from the cache, we never want 181 | ;; insert or update hooks to be called. Snabbdom will 182 | ;; occasionally call these even when there are no changes, 183 | ;; because it uses identity to determine if a vdom node 184 | ;; represents a change. Since dumdom always produces a new 185 | ;; JavaScript object, Snabbdom's check will have false 186 | ;; positives. 187 | (some-> rendered 188 | #?(:cljs (setup-mount-hook opt data args animation)) 189 | #?(:cljs (setup-update-hook opt data (:data instance) args)))) 190 | (:vdom instance))))] 191 | #?(:cljs (set! (.-dumdom comp-fn) true)) 192 | comp-fn))))) 193 | 194 | (defn single-child? [x] 195 | (or (fn? x) ;; component 196 | (and (vector? x) 197 | (keyword? (first x))) ;; hiccup 198 | )) 199 | 200 | (defn TransitionGroup [el-fn opt children] 201 | ;; Vectors with a function in the head position are interpreted as hiccup data 202 | ;; - force children to be seqs to avoid them being parsed as hiccup. 203 | (let [children (if (single-child? children) 204 | (list children) 205 | (seq children))] 206 | (if (ifn? (:component opt)) 207 | ((:component opt) children) 208 | (apply el-fn (or (:component opt) "span") opt children)))) 209 | 210 | (defn- complete-transition [node timeout callback] 211 | (if timeout 212 | #?(:cljs (js/setTimeout callback timeout)) 213 | (let [callback-fn (atom nil) 214 | f (fn [] 215 | (callback) 216 | (.removeEventListener node "transitionend" @callback-fn))] 217 | (reset! callback-fn f) 218 | (.addEventListener node "transitionend" f)))) 219 | 220 | (defn- transition-classes [transitionName transition] 221 | (if (string? transitionName) 222 | [(str transitionName "-" transition) (str transitionName "-" transition "-active")] 223 | (let [k (keyword transition) 224 | k-active (keyword (str transition "Active"))] 225 | [(k transitionName) (get transitionName k-active (str (k transitionName) "-active"))]))) 226 | 227 | (defn- animate [transition {:keys [enabled-by-default?]}] 228 | (let [timeout (keyword (str "transition" transition "Timeout"))] 229 | (fn [node callback {:keys [transitionName] :as props}] 230 | (if (get props (keyword (str "transition" transition)) enabled-by-default?) 231 | (let [[init-class active-class] (transition-classes transitionName (.toLowerCase transition))] 232 | (.add (.-classList node) init-class) 233 | (complete-transition node (get props timeout) callback) 234 | #?(:cljs (js/setTimeout #(.add (.-classList node) active-class) 0))) 235 | (callback))))) 236 | 237 | (defn- cleanup-animation [transition] 238 | (fn [node {:keys [transitionName]}] 239 | (.remove (.-classList node) (str transitionName "-" transition)) 240 | (.remove (.-classList node) (str transitionName "-" transition "-active")))) 241 | 242 | (def TransitioningElement 243 | (component 244 | (fn [{:keys [child]}] 245 | child) 246 | {:will-appear (animate "Appear" {:enabled-by-default? false}) 247 | :did-appear (cleanup-animation "appear") 248 | :will-enter (animate "Enter" {:enabled-by-default? true}) 249 | :did-enter (cleanup-animation "enter") 250 | :will-leave (animate "Leave" {:enabled-by-default? true})})) 251 | 252 | (defn CSSTransitionGroup [el-fn opt children] 253 | (let [children (if (single-child? children) 254 | (list children) 255 | (seq children))] 256 | (TransitionGroup el-fn opt (map #(TransitioningElement (assoc opt :child %)) children)))) 257 | 258 | (defn component? [x] 259 | (and x (.-dumdom x))) 260 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # dumdom - The dumb DOM component library 2 | 3 | **dumdom** is a component library that renders (and re-renders) immutable data 4 | efficiently. It delivers on the basic value proposition of React and its peers 5 | while eschewing features like component local state and object oriented APIs, 6 | and embracing ClojureScript features like immutable data structures. 7 | 8 | **dumdom** is API compatible with 9 | [Quiescent](https://github.com/levand/quiescent/), and can be used as a drop-in 10 | replacement for it so long as you don't use React features directly. Refer to 11 | [differences from React](#differences-from-react) for things to be aware of. 12 | 13 | **dumdom** is currently a wrapper for 14 | [Snabbdom](https://github.com/snabbdom/snabbdom), but that should be considered 15 | an implementation detail, and may be subject to change. Using snabbdom features 16 | not explicitly exposed by dumdom is **not** recommended. 17 | 18 | **dumdom** aims to be finished, stable, and worthy of your trust. Breaking 19 | changes will never be intentionally introduced to the codebase. For this reason, 20 | dumdom does not adhere to the "semantic" versioning scheme. 21 | 22 | In addition to being API compatible with Quiescent, **dumdom** supports: 23 | 24 | - Rendering to strings (useful for server-side rendering from both the JVM and node.js) 25 | - Efficient "inflation" of server-rendered markup on the client side 26 | - Hiccup syntax for components 27 | 28 | ## Table of contents 29 | 30 | * [Install](#install) 31 | * [Example](#example) 32 | * [Rationale](#rationale) 33 | * [Limitations](#limitations) 34 | * [Differences from Quiescent](#differences-from-quiescent) 35 | * [Using with Devcards](#using-with-devcards) 36 | * [Contribute](#contribute) 37 | * [Documentation](#documentation) 38 | * [Building virtual DOM](#building-virtual-dom) 39 | * [Event listeners](#event-listeners) 40 | * [Creating components](#creating-components) 41 | * [CSS transitions](#css-transitions) 42 | * [Class name transitions](#class-name-transitions) 43 | * [Refs](#refs) 44 | * [Server-rendering](#server-rendering) 45 | * [API Docs](#api-docs) 46 | * [Examples](#examples) 47 | * [Changelog](#changelog) 48 | * [Roadmap](#roadmap) 49 | * [License](#license) 50 | 51 | ## Install 52 | 53 | With tools.deps: 54 | 55 | ```clj 56 | cjohansen/dumdom {:mvn/version "2021.06.21"} 57 | ``` 58 | 59 | With Leiningen: 60 | 61 | ```clj 62 | [cjohansen/dumdom "2021.06.21"] 63 | ``` 64 | 65 | ## Example 66 | 67 | Using hiccup-style data: 68 | 69 | ```clj 70 | (require '[dumdom.core :as dumdom :refer [defcomponent]]) 71 | 72 | (defcomponent heading 73 | :on-render (fn [dom-node val old-val]) 74 | [data] 75 | [:h2 {:style {:background "#000"}} (:text data)]) 76 | 77 | (defcomponent page [data] 78 | [:div 79 | [heading (:heading data)] 80 | [:p (:body data)]]) 81 | 82 | (dumdom/render 83 | [page {:heading {:text "Hello world"} 84 | :body "This is a web page"}] 85 | (js/document.getElementById "app")) 86 | ``` 87 | 88 | Using the Quiescent-compatible function API: 89 | 90 | ```clj 91 | (require '[dumdom.core :as dumdom :refer [defcomponent]] 92 | '[dumdom.dom :as d]) 93 | 94 | (defcomponent heading 95 | :on-render (fn [dom-node val old-val]) 96 | [data] 97 | (d/h2 {:style {:background "#000"}} (:text data))) 98 | 99 | (defcomponent page [data] 100 | (d/div {} 101 | (heading (:heading data)) 102 | (d/p {} (:body data)))) 103 | 104 | (dumdom/render 105 | (page {:heading {:text "Hello world"} 106 | :body "This is a web page"}) 107 | (js/document.getElementById "app")) 108 | ``` 109 | 110 | ## Rationale 111 | 112 | Of the many possible options, [Quiescent](https://github.com/levand/quiescent) 113 | is to me the perfect expression of "React in ClojureScript". It's simple, 114 | light-weight, does not allow component-local state, and pitches itself as 115 | strictly a rendering library, not a state management tool or UI framework. 116 | 117 | While Quiescent has been done (as in "complete") for a long time, it is built on 118 | React, which is on a cycle of recurring "deprecations" and API changes, making 119 | it hard to keep Quiescent up to date with relevant security patches etc. At the 120 | same time, React keeps adding features which are of no relevance to the API 121 | Quiescent exposes, thus growing the total bundle size for no advantage to 122 | its users. 123 | 124 | **dumdom** provides the same API as that of Quiescent, but does not depend on 125 | React. It aims to be as stable and complete as Quiescent, but still be able to 126 | ship occasional security patches as they are made to the underlying virtual DOM 127 | library. **dumdom** aims to reduce the amount of churn in your UI stack. 128 | 129 | ## Limitations 130 | 131 | Because **dumdom** is not based on React, you opt out of the "React ecosystem" 132 | entirely by using it. If you depend on a lot of open source/shared React 133 | components, or other React-oriented tooling, **dumdom** might not be the best 134 | fit for you. 135 | 136 | Because **dumdom** does not offer any kind of component local state, it cannot 137 | be used as a wholistic UI framework - it's just a rendering library. It does not 138 | come with any system for routing, dispatching actions, or managing state (either 139 | inside or outside of components), and is generally a batteries-not-included 140 | tool. I consider this a strength, others may see it differently. 141 | 142 | ## Differences from Quiescent 143 | 144 | Dumdom strives to be API compliant with Quiescent to the degree that it should 145 | be a drop-in replacement for Quiescent in any project that does not rely 146 | explicitly on any React APIs or third-party components. It does not necessarily 147 | commit to all the same restrictions that the Quiescent API imposes. The 148 | following is a list of minor details between the two: 149 | 150 | - Quiescent does not allow the use of `:on-render` along with either of 151 | `:on-mount` and `:on-update`. Dumdom acknowledges that some components will 152 | implement `:on-render` *and* `:on-mount` or `:on-update`, and allows this. 153 | - Dumdom doesn't really care about `TransitionGroup`. You are free to use them, 154 | but the animation callbacks will work equally well outside `TransitionGroup`. 155 | This may cause breakage in some cases when porting from Quiescent to Dumdom. 156 | The risk is pretty low, and the upside is significant enough to allow Dumdom 157 | to take this liberty. 158 | 159 | ## Differences from React 160 | 161 | In React, [`onChange` is really 162 | `onInput`](https://github.com/facebook/react/issues/9567). This is not true in 163 | dumdom. When swapping out Quiescent and React for dumdom, you must replace 164 | all occurrences of `onChange` with `onInput` to retain behavior. 165 | 166 | ## Using with Devcards 167 | 168 | [Devcards](https://github.com/bhauman/devcards) is a system for rendering React 169 | components in isolation. Because **dumdom** components are not React components, 170 | they need some wrapping for Devcards to make sense of them. 171 | 172 | You need to add [dumdom-devcards](https://github.com/cjohansen/dumdom-devcards) 173 | as a separate dependency. Then use the `dumdom.devcards` namespace just like you 174 | would `devcards.core`: 175 | 176 | ```clj 177 | (require '[dumdom.devcards :refer-macros [defcard]]) 178 | 179 | (defcard my-dumdom-card 180 | (my-dumdom-component {:value 0})) 181 | ``` 182 | 183 | ## Contribute 184 | 185 | Feel free to report bugs and, even better, provide bug fixing pull requests! 186 | Make sure to add tests for your fixes, and make sure the existing ones stay 187 | green before submitting fixes. 188 | 189 | ```sh 190 | make test 191 | ``` 192 | 193 | You can also run the tests in a browser with figwheel, which might be more 194 | useful during development: 195 | 196 | ```sh 197 | clojure -A:dev:repl 198 | ``` 199 | 200 | Then open [http://localhost:9595/figwheel-extra-main/tests](http://localhost:9595/figwheel-extra-main/tests). 201 | 202 | If you're not yet sure how to formulate a test for your feature, fire up 203 | [http://localhost:9595/](http://localhost:9595/) and play around in 204 | [./dev/dumdom/dev.cljs](./dev/dumdom/dev.cljs) until you figure it out. More 205 | visually oriented code can be tested with devcards instead. Add a devcard to 206 | [./devcards/dumdom](./devcards/dumdom), and inspect the results at 207 | [http://localhost:9595/devcards.html](http://localhost:9595/devcards.html) 208 | 209 | If you have ideas for new features, please open an issue to discuss the idea and 210 | the API before implementing it to avoid putting lots of work into a pull request 211 | that might be rejected. I intend to keep **dumdom** a focused package, and don't 212 | want it to accrete a too wide/too losely coherent set of features. 213 | 214 | ### Running from Emacs 215 | 216 | There is a `.dir-locals.el` file in the root of this repo to help you out. Run 217 | `cider-jack-in-cljs`, and you should get a REPL and figwheel running on port 9595: 218 | 219 | - [Dev scratchpad](http://localhost:9595/) 220 | - [Devcards](http://localhost:9595/devcards.html) 221 | - [Tests](http://localhost:9595/figwheel-extra-main/tests) 222 | 223 | ## Documentation 224 | 225 | The vast majority of use-cases are covered by using [hiccup-style markup]() for 226 | DOM elements, defining custom components with [`defcomponent`](#defcomponent), 227 | and rendering the resulting virtual DOM to an element with [`render`](#render): 228 | 229 | ```clj 230 | (require '[dumdom.core :as dumdom :refer [defcomponent]]) 231 | 232 | (defcomponent my-component [data] 233 | [:div 234 | [:h1 "Hello world!"] 235 | [:p (:message data)]]) 236 | 237 | (dumdom/render 238 | (my-component {:message "Hello, indeed"}) 239 | (js/document.getElementById "app")) 240 | ``` 241 | 242 | Components defined by `defcomponent` are functions, as demonstrated in the above 243 | example. You can also use them for hiccup markup, e.g.: 244 | 245 | ```clj 246 | (dumdom/render 247 | [my-component {:message "Hello, indeed"}] 248 | (js/document.getElementById "app")) 249 | ``` 250 | 251 | The strength of hiccup markup is being able to represent DOM structures as pure 252 | data. Because functions are not data, there is no real benefit to using hiccup 253 | syntax for custom components, so I typically don't, but it doesn't make any 254 | difference either way. 255 | 256 | ### Building virtual DOM 257 | 258 | Virtual DOM elements are built with hiccup markup: 259 | 260 | ```clj 261 | [tagname attr? children...] 262 | ``` 263 | 264 | `tagname` is always a keyword, attributes are in an optional map, and there 265 | might be one or more children, or a list of children. Beware that children 266 | should not be provided as a vector, lest it be interpreted as a new hiccup 267 | element. 268 | 269 | **Note:** dumdom currently does not support inlining class names and ids on the 270 | tag name selector (e.g. `:div.someclass#someid`). This might be added in a 271 | future release. 272 | 273 | For API compatibility with Quiescent, elements can also be created with the 274 | functions in `dumdom.dom`: 275 | 276 | ```clj 277 | (dumdom.dom/div {:style {:border "1px solid red"}} "Hello world") 278 | ``` 279 | 280 | Note that with these functions, the attribute map is not optional, and must 281 | always be provided, even if empty. 282 | 283 | #### Keys 284 | 285 | You can specify the special attribute `:key` do help dumdom recognize DOM 286 | elements that move. `:key` should be set to a value that is unique among the 287 | element's siblings. For instance, if you are rendering lists of things, setting 288 | a key on each item means dumdom can update the rendered view by simply moving 289 | existing elements around in the DOM. Not setting the key will lead dumdom to 290 | work harder to align the DOM with the virtual representation: 291 | 292 | ```clj 293 | (require '[dumdom.core :as dumdom :refer [defcomponent]]) 294 | 295 | (defcomponent list-item [fruit] 296 | [:li {:key fruit} fruit]) 297 | 298 | (def el (js/document.getElementById "app")) 299 | 300 | (dumdom/render [:ul (map list-item ["Apples" "Oranges" "Kiwis"])] el) 301 | 302 | ;; This will now result in reordering the DOM elements, instead of recreating them 303 | (dumdom/render [:ul (map list-item ["Oranges" "Apples" "Kiwis"])] el) 304 | ``` 305 | 306 | ### Event listeners 307 | 308 | To attach events to your virtual DOM nodes, provide functions to camel-cased 309 | event name keys in the attribute map: 310 | 311 | ```clj 312 | [:a {:href "#" 313 | :onClick (fn [e] 314 | (.preventDefault e) 315 | (prn "You clicked me!"))} "Click me!"] 316 | ``` 317 | 318 | ### Creating components 319 | 320 | You create components with `defcomponent` or `component` - the first is 321 | just a convenience macro for `def` + `component`: 322 | 323 | ```clj 324 | (require '[dumdom.core :refer [component defcomponent]]) 325 | 326 | (defcomponent my-component 327 | :on-render (fn [e] (js/console.log "Rendered" e)) 328 | [data] 329 | [:div "Hello world"]) 330 | 331 | ;; ...is the same as: 332 | 333 | (def my-component 334 | (component 335 | (fn [data] 336 | [:div "Hello world"]) 337 | {:on-render (fn [e] (js/console.log "Rendered" e))})) 338 | ``` 339 | 340 | Refer to the API docs for [`component`](#component) for details on what options 341 | it supports, life-cycle hooks etc, and the API docs for 342 | [`defcomponent`](#defcomponent) for more on how to use it. 343 | 344 | A dumdom component is a function. When you call it with data it returns 345 | something that dumdom knows how to render, e.g.: 346 | 347 | ```clj 348 | (dumdom.core/render (my-component {:id 42}) root-el) 349 | ``` 350 | 351 | You can also invoke the component with hiccup markup, although there is no real 352 | benefit to doing so - the result is exactly the same: 353 | 354 | ```clj 355 | (dumdom.core/render [my-component {:id 42}] root-el) 356 | ``` 357 | 358 | #### Component arguments 359 | 360 | When you call a dumdom component with data, it will recreate the virtual DOM 361 | node only if the data has changed since it was last called. However, this 362 | decision is based solely on the first argument passed to the component. So while 363 | you can pass any number of arguments to a component beware that only the first 364 | one is used to influence rendering decisions. 365 | 366 | This design is inherited from Quiescent, and the idea is that you can pass along 367 | things like core.async message channels without having them interferring with 368 | the rendering decisions. When passing more than one argument to a dumdom 369 | component, make sure that any except the first one are constant for the lifetime 370 | of the component. 371 | 372 | This only applies to components created with `component`/`defcomponent`, not 373 | virtual DOM functions, which take any number of DOM children. 374 | 375 | ### CSS transitions 376 | 377 | CSS transitions can be defined inline on components to animate the appearing or 378 | disappearing of elements. There are three keys you can use to achieve this 379 | effect: 380 | 381 | - `:mounted-style` - Styles that will apply after the element has been mounted 382 | - `:leaving-style` - Styles that will apply before the element is removed from 383 | its parent - the element will not be removed until all its transitions 384 | complete 385 | - `:disappearing-style` - Styles that will apply before the element is removed 386 | along with its parent element is being removed - the element will not be 387 | removed until all its transitions are complete 388 | 389 | As an example, if you want an element to fade in, set its opacity to 0, and then 390 | its `:mounted-style` opacity to 1. To fade it out as well, set its 391 | `:leaving-styles` opacity to 0 again. Remember to enable transitions for the 392 | relevant CSS property: 393 | 394 | ```clj 395 | [:div {:style {:opacity "0" 396 | :transition "opacity 0.25s"} 397 | :mounted-style {:opacity "1"} 398 | :leaving-style {:opacity "0"}} 399 | "I will fade both in and out"] 400 | ``` 401 | 402 | ### Class name transitions 403 | 404 | In order to be API compatible with Quiescent, dumdom supports React's 405 | `CSSTransitionGroup` for doing enter/leave transitions with class names instead 406 | of inline CSS. Given the following CSS: 407 | 408 | ```css 409 | .example-leave { 410 | opacity: 1; 411 | transition: opacity 0.25s; 412 | } 413 | 414 | .example-leave-active { 415 | opacity: 0; 416 | } 417 | ``` 418 | 419 | Then we could fade out an element with: 420 | 421 | ```clj 422 | (require '[dumdom.core :refer [CSSTransitionGroup]]) 423 | 424 | (CSSTransitionGroup {:transitionName "example"} 425 | [[:div "I will fade out"]]) 426 | ``` 427 | 428 | Note that `CSSTransitionGroup` takes a vector/seq of children. Refer to the 429 | [API docs for `CSSTransitionGroup`](#css-transition-group) for more details. In 430 | general, using inline CSS transitions will be more straight-forward, and is 431 | recommended. 432 | 433 | ### Refs 434 | 435 | A `:ref` on an element is like an `:on-mount` callback that you can attach from 436 | "the outside": 437 | 438 | ```clj 439 | ;; NB! Just an example, there are better ways to do this with CSS 440 | 441 | (defn square-element [el] 442 | (set! (.. el -style -height) (str (.-offsetWidth el) "px"))) 443 | 444 | [:div {:style {:border "1px solid red"} 445 | :ref square-element} "I will be in a square box"] 446 | ``` 447 | 448 | The `:ref` function will be called only once, when the element is first mounted. 449 | Use this feature with care - do not use it with functions that behave 450 | differently at different times. Consider this example: 451 | 452 | ```clj 453 | (defcomponent my-component [data] 454 | [:div 455 | [:h1 "Example"] 456 | [:div {:ref (when (:actionable? data) 457 | setup-click-indicator)} 458 | "I might or might not be clickable"]]) 459 | ``` 460 | 461 | While this looks reasonable, refs are only called when the element mounts. Thus, 462 | if the value of `(:actionable? data)` changes, the changes will not be reflected 463 | on the element. If you need to conditionally make changes to an element this 464 | way, create a custom component and use the `:on-render` hook instead, which is 465 | called every time data changes. 466 | 467 | ### Server rendering 468 | 469 | Dumdom supports rendering your components to strings on the server and then 470 | "inflating" the view client-side. Inflating consists of associating the 471 | resulting DOM elements with their respective virtual DOM nodes, so dumdom can 472 | efficiently update your UI, and adding client-side event handlers so users can 473 | interact with your app. 474 | 475 | Even though it sounds straight-forward, using server rendering requires that 476 | you write your entire UI layer in a way that can be loaded on both the server 477 | and client. This is easier said than done. 478 | 479 | To render your UI to a string on the server: 480 | 481 | ```clj 482 | (require '[dumdom.string :as dumdom]) 483 | 484 | (defn body [] 485 | (str "
    " 486 | (dumdom/render [:div [:h1 "Hello world"]]) 487 | "
    ")) 488 | 489 | (defn index [req] 490 | {:status 200 491 | :headers {"content-type" "text/html"} 492 | :body (body)}) 493 | ``` 494 | 495 | Then, on the client: 496 | 497 | ```clj 498 | (require '[dumdom.inflate :as dumdom]) 499 | 500 | (dumdom/render 501 | [:div [:h1 "Hello world]] 502 | (js/document.getElementById "app")) 503 | ``` 504 | 505 | To update your view, either call `dumdom.inflate/render` again, or use 506 | `dumdom.core/render`. 507 | 508 | ### API Docs 509 | 510 | 511 | #### `(dumdom.core/render component element)` 512 | 513 | Render the virtual DOM node created by the component into the specified DOM 514 | element. Component can be either hiccup-style data, like `[:div {} "Hello"]` or 515 | the result of calling component functions, e.g. `(dumdom.dom/div {} "Hello")`. 516 | 517 | 518 | #### `(dumdom.core/unmounet element)` 519 | 520 | Clear the element and discard any internal state related to it. 521 | 522 | 523 | #### `(dumdom.core/render-once component element)` 524 | 525 | Like `dumdom.core/render`, but entirely stateless. `render` needs to use memory 526 | in order for subsequent calls to render as little as possible as fast as 527 | possible. If you don't intend to update the rendered DOM structure, 528 | `render-once` is more efficient as it does not use any memory. Subsequent calls 529 | to this function with the same arguments will always destructively re-render the 530 | entire tree represented by the component. 531 | 532 | 533 | #### `(dumdom.core/component render-fn [opt])` 534 | 535 | Returns a component that uses the provided function for rendering. The resulting 536 | component will only call through to its rendering function when called with data 537 | that is different from the data that produced the currently rendered version of 538 | the component. 539 | 540 | The rendering function can be called with any number of arguments, but only the 541 | first one will influence rendering decisions. You should call the component with 542 | a single immutable value, followed by any number of other arguments, as desired. 543 | These additional constant arguments are suitable for passing messaging channels, 544 | configuration maps, and other utilities that are constant for the lifetime of 545 | the rendered element. 546 | 547 | The rendering function can return hiccup-style data or the result of calling 548 | component functions. 549 | 550 | The optional opts argument is a map with additional properties: 551 | 552 | `:on-mount` - A function invoked once, immediately after initial rendering. It 553 | is passed the rendered DOM node, and all arguments passed to the render 554 | function. 555 | 556 | `:on-update` - A function invoked immediately after an updated is flushed to the 557 | DOM, but not on the initial render. It is passed the underlying DOM node, the 558 | value, and any constant arguments passed to the render function. 559 | 560 | `:on-render` - A function invoked immediately after the DOM is updated, both on 561 | the initial render and subsequent updates. It is passed the underlying DOM node, 562 | the value, the old value, and any constant arguments passed to the render 563 | function. 564 | 565 | `:on-unmount` - A function invoked immediately before the component is unmounted 566 | from the DOM. It is passed the underlying DOM node, the most recent value and 567 | the most recent constant args passed to the render fn. 568 | 569 | `:will-appear` - A function invoked when this component is added to a mounting 570 | container component. Invoked at the same time as :on-mount. It is passed the 571 | underlying DOM node, a callback function, the most recent value and the most 572 | recent constant args passed to the render fn. The callback should be called to 573 | indicate that the element is done \"appearing\". 574 | 575 | `:did-appear` - A function invoked immediately after the callback passed to 576 | :will-appear is called. It is passed the underlying DOM node, the most recent 577 | value, and the most recent constant args passed to the render fn. 578 | 579 | `:will-enter` - A function invoked when this component is added to an already 580 | mounted container component. Invoked at the same time as :on.mount. It is passed 581 | the underlying DOM node, a callback function, the value and any constant args 582 | passed to the render fn. The callback function should be called to indicate that 583 | the element is done entering. 584 | 585 | `:did-enter` - A function invoked after the callback passed to :will-enter is 586 | called. It is passed the underlying DOM node, the value and any constant args 587 | passed to the render fn. 588 | 589 | `:will-leave` - A function invoked when this component is removed from its 590 | containing component. Is passed the underlying DOM node, a callback function, 591 | the most recent value and the most recent constant args passed to the render fn. 592 | The DOM node will not be removed until the callback is called. 593 | 594 | `:did-leave` - A function invoked after the callback passed to :will-leave is 595 | called (at the same time as :on-unmount). Is passed the underlying DOM node, the 596 | most recent value and the most recent constant args passed to the render fn. 597 | 598 | 599 | #### `(dumdom.core/defcomponent name & args)` 600 | 601 | Creates a component with the given name, a docstring (optional), any number of 602 | option->value pairs (optional), an argument vector and any number of forms body, 603 | which will be used as the rendering function to dumdom.core/component. 604 | 605 | For example: 606 | 607 | ```clj 608 | (defcomponent widget 609 | \"A Widget\" 610 | :on-mount #(...) 611 | :on-render #(...) 612 | [value constant-value] 613 | (some-child-components)) 614 | ``` 615 | 616 | Is shorthand for: 617 | 618 | ```clj 619 | (def widget (dumdom.core/component 620 | (fn [value constant-value] (some-child-components)) 621 | {:on-mount #(...) 622 | :on-render #(...)})) 623 | ``` 624 | 625 | #### `(dumdom.core/TransitionGroup opt children)` 626 | 627 | Exists solely for drop-in compatibility with Quiescent. Effectively does 628 | nothing. Do not use for new applications. 629 | 630 | 631 | #### `(dumdom.core/CSSTransitionGroup opt children)` 632 | 633 | Automates animation of entering and leaving elements via class names. If called 634 | with `{:transitionName "example"}` as `opt`, child elements will have class 635 | names set on them at appropriate times. 636 | 637 | When the transition group mounts, all pre-existing children will have the class 638 | name `example-enter` set on them. Then, `example-enter-active` is set. When all 639 | transitions complete on the child node, `example-enter-active` will be removed 640 | again. 641 | 642 | When elements are added to an already mounted transition group, they will have 643 | the class name `example-appear` added to them, _if appear animations are 644 | enabled_ (they are not by default). Then the class name `example-appear-active` 645 | will be set, and then removed after all transitions complete. 646 | 647 | When elements are removed from the transition group, the class name 648 | `example-leave` will be set, followed by `example-leave-active`, which is then 649 | removed after transitions complete. 650 | 651 | You can control which transitions are used on elements, and how their classes 652 | are named with the following options: 653 | 654 | ##### `transitionName` 655 | 656 | When set to a string: base-name for all classes. Can also be set to a map to 657 | control individual class names: 658 | 659 | ```clj 660 | {:transitionName {:enter "entrance"}} ;; entrance / entrance-active 661 | {:transitionName {:enter "enter" :enterActive "entering"}} enter / entering 662 | ``` 663 | 664 | And similarly for `:leave`/`:leaveActive` and `:appear`/`:appearActive`. 665 | 666 | ##### `transitionEnter` 667 | 668 | Boolean, set to `false` to disable enter transitions. Defaults to `true`. 669 | 670 | ##### `transitionAppear` 671 | 672 | Boolean, set to `true` to enable appear transitions. Defaults to `false`. 673 | 674 | ##### `transitionLeave` 675 | 676 | Boolean, set to `false` to disable leave transitions. Defaults to `true`. 677 | 678 | #### `(dumdom.dom/[el] attr children)` 679 | 680 | Functions are defined for every HTML element: 681 | 682 | ```clj 683 | (dumdom.dom/a {:href "https://cjohansen.no/"} "Blog") 684 | ``` 685 | 686 | Attributes are **not** optional, use an empty map if you don't have attributes. 687 | Children can be text, components, virtual DOM elements (like the one above), or 688 | a seq with a mix of those. 689 | 690 | #### `(dumdom.core/render-string component)` 691 | 692 | Renders component to string. Available on Clojure as well, and can be used to do 693 | server-side rendering of dumdom components. 694 | 695 | #### `(dumdom.inflate/render component el)` 696 | 697 | Renders the component into the provided element. If `el` contains 698 | server-rendered dumdom components, it will be inflated faster than a fresh 699 | render (which forcefully rebuilds the entire DOM tree). 700 | 701 | **NB!** Currently, only string keys are supported. If a component uses 702 | non-string keys, inflating will not work, and it will be forcefully re-rendered. 703 | This limitation might be adressed in a future release. 704 | 705 | 706 | #### `dumdom.component/*render-eagerly?*` 707 | 708 | When this var is set to `true`, every existing component will re-render on the 709 | next call after a new component has been created, even if the input data has not 710 | changed. This can be useful in development - if you have any level of 711 | indirection in your rendering code (e.g. passing a component function as the 712 | "static arg" to another component, multi-methods, etc), you are not guaranteed 713 | to have all changed components re-render after a compile and hot swap. With this 714 | var set to `true`, changing any code that defines a dumdom component will cause 715 | all components to re-render. 716 | 717 | The var defaults to `false`, in which case it has no effect. Somewhere in your 718 | development setup, add 719 | 720 | ```clj 721 | (set! dumdom.component/*render-eagerly?* true) 722 | ``` 723 | 724 | ## Examples 725 | 726 | Unfortunately, there is no TodoMVC implementation yet, but there is 727 | [Yahtzee](https://github.com/cjohansen/yahtzee-cljs/)! Please get in touch if you've 728 | used dumdom for anything and I'll happily include a link to your app. 729 | 730 | Check out this cool [dungeon crawler](http://heck.8620.cx/) 731 | ([source](https://github.com/uosl/heckendorf)) made with dumdom. 732 | 733 | ## Changelog 734 | 735 | ### 2021.06.28 736 | 737 | - Force elements with innerHTML to have a key. Works around shortcomings in 738 | Snabbdom related to innerHTML manipulation. 739 | - New featyre: `dumdom.core/unmount` (see [docs above](#unmount)). 740 | - New feature: `dumdom.core/render-once` (see [docs above](#render-once)). 741 | - Major implementation change: Move Dumdom's vdom representation from Snabbdom's 742 | object model to Clojure maps. This moves more of the implementation from 743 | JavaScript to Clojure, and more importantly addresses som weird behavior in 744 | complex DOM layouts. The vdom objects created by Snabbdom are mutated by 745 | Snabbdom to maintain a reference to the rendered elements. In other words, the 746 | vdom objects we provide as input to Snabbdom ends up doubling as Snabbdom's 747 | internal state. Hanging on to these from the outside and feeding them back to 748 | Snabbdom at a later point (e.g. when `should-component-update?` is `false`) 749 | _mostly_ works, but has been proven to cause very unfortunate behavior in 750 | certain bespoke situations. 751 | 752 | As always, there are no changes to the public API. However, this is a invasive 753 | change to Dumdom's implementation, so rigorous testing is adviced. 754 | 755 | **NB!** The internal data-structure of virtual DOM nodes have changed as a 756 | consequence, both for Clojure (different map structure) and ClojureScript 757 | (maps, not Snabbdom objects). This is considered implementation changes, as 758 | these are not documented features of Dumdom. If you have somehow ended up 759 | relying on those you should investigate changes further. 760 | 761 | ### 2021.06.21 762 | 763 | - Render comment nodes in place of `nil`s. This works around a quirk of Snabbdom 764 | (as compared to React) where replacing a `nil` with an element can prematurely 765 | cause transition effects due to how Snabbdom reuses DOM elements. See [this 766 | issue](https://github.com/snabbdom/snabbdom/issues/973) for more information. 767 | 768 | ### 2021.06.18 769 | 770 | - Retracted due to a typo which made the artefact unusable. Sorry about that! 771 | 772 | ### 2021.06.16 773 | 774 | - BREAKING: Dumdom no longer bundles `dumdom.devcards` - add 775 | [dumdom-devcards](https://github.com/cjohansen/dumdom-devcards) separately to 776 | your dependencies. This change only affects projects using `dumdom.devcards`, 777 | and requires no other changes to your code than adding the separate namespace. 778 | I'm very sorry for this breaking change, but including devcards was a mistake 779 | that required you to have devcards on path in production, which is not a good 780 | place to be. 781 | - Bug fix: `{:dangerouslySetInnerHTML {:__html nil}}` was mistakenly a noop. It 782 | now clears any previously set `innerHTML`. 783 | - Work around [a bug in Snabbdom](https://github.com/snabbdom/snabbdom/issues/970) 784 | by temporarily bundling a patched version. 785 | 786 | ### 2021.06.11 787 | 788 | - Properly support data-attributes. Just include them with the data- prefix, and 789 | dumdom will render them: `[:div {:data-id "123"} "Hello"]`. 790 | 791 | ### 2021.06.10 792 | 793 | - Upgrade Snabbdom to version 3.0.3 794 | - Make sure all style properties are strings. Fixes a strange glitch where 795 | `:opacity 0` would not always set opacity to 0. 796 | 797 | ### 2021.06.08 798 | 799 | - Pixelize more styles, switch from a allow-list to a deny-list of properties to 800 | pixelize. Thanks to [Magnar Sveen](https://github.com/magnars). 801 | 802 | ### 2021.06.07 803 | 804 | - Add feature to eagerly re-render components in development, see 805 | `*render-eagerly?*` above. 806 | 807 | - Pass old data to `:on-render` and `:on-update` functions. Previously, these 808 | would receive `[dom-el data statics]`, now they fully match Quiescent's 809 | behavior, receiving `[dom-el data old-data statics]`. 810 | 811 | ### 2021.06.02 812 | 813 | - Allow `TransitionGroup` to take either a single component or a seq of 814 | components. 815 | 816 | ### 2021.06.01 817 | 818 | - Bugfix: Don't throw exceptions when components return nil 819 | - If the root component returns nil, remove previously rendered content. 820 | 821 | ### 2021.05.31 822 | 823 | - Support pixel values for `:border`, `:border-left`, `:border-right`, 824 | `:border-top`, `:border-bottom`. 825 | 826 | ### 2021.05.28 827 | 828 | - Bug fix: Using non-primitive values (including keywords) with component keys 829 | (both inline `:key` and the result from `:keyfn`) would cause weird rendering 830 | issues. Any value can now be safely used as a component key. 831 | - Bug fix: CSS properties that take pixel values could only be specified as 832 | numbers when the camelCased property name was used. Now also supports this 833 | behavior with lisp-casing, e.g. `:style {:margin-left 10}` produces a 10 pixel 834 | left margin. 835 | - Bug fix: Support numbers for `border-{bottom,top}-{left,right}-radius` (and 836 | their camel cased counterparts). 837 | - Remove an attempt at a micro-optimization that instead caused a minimal 838 | performance penalty. 839 | 840 | ### 2021.05.07 841 | 842 | - Fix failing production builds of 2020.10.19 due to missing externs 843 | 844 | ### 2020.10.19 845 | 846 | - Add support for Shadow CLJS 847 | 848 | ### 2020.07.04 849 | 850 | - Properly render nested seqs to DOM strings 851 | 852 | ### 2020.06.21 853 | 854 | - Don't render `:ref` functions to the DOM 855 | - Don't render `nil` styles when rendering to strings 856 | 857 | ### 2020.06.04 858 | 859 | - Added support for styles as strings in hiccup elements, e.g. 860 | `[:a {:style "padding: 2px"}]` 861 | 862 | ### 2020.02.12 863 | 864 | - Bugfix: Don't trip on numbers when rendering to string 865 | - Bugfix: Don't trip on event handlers when rendering to string 866 | 867 | ### 2020.01.27 868 | 869 | - Added support for `:dangerouslySetInnerHTML` 870 | 871 | ### 2019.09.16 872 | 873 | - Fixed animation style properties, which where inadvertently broken in the 874 | previous release :'( 875 | 876 | ### 2019.09.05-1 877 | 878 | - Support using dashed cased attributes, e.g. `:xlink-href`, `:view-box` etc 879 | - When passing `nil` to an attribute, do not render that attribute with the 880 | string "null" or an empty value - remove the attribute 881 | 882 | ### 2019.09.05 883 | 884 | - Support `:div.class#id` style hiccup in server-rendering as well. 885 | 886 | ### 2019.02.03-3 887 | 888 | - Built jar with a different version of `pack.alpha`, so 889 | [cljdoc](https://cljdoc.org) is able to analyze it 890 | 891 | ### 2019.01.21 892 | 893 | - Document and launch `:mounted-style`, `:leaving-style`, and `:disappearing-style` 894 | 895 | ### 2019.01.19 896 | 897 | - Added support for hiccup-style data 898 | - Added rendering components to strings 899 | - Added inflating server-rendered DOM 900 | 901 | ### 2018.12.22 902 | 903 | - Added snabbdom externs that hold up during advanced compilation 904 | 905 | ### 2018.12.21 906 | 907 | Initial release 908 | 909 | ## Roadmap 910 | 911 | - Provide TodoMVC app 912 | - Port Snabbdom (roughly, not API compatibly) to ClojureScript 913 | 914 | ## License 915 | 916 | Copyright © 2018-2021 Christian Johansen 917 | 918 | Distributed under the Eclipse Public License either version 1.0 or (at your 919 | option) any later version. 920 | -------------------------------------------------------------------------------- /src/snabbdom/snabbdom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var snabbdom = {}; 4 | 5 | function createElement(tagName, options) { 6 | return document.createElement(tagName, options); 7 | } 8 | function createElementNS(namespaceURI, qualifiedName, options) { 9 | return document.createElementNS(namespaceURI, qualifiedName, options); 10 | } 11 | function createTextNode(text) { 12 | return document.createTextNode(text); 13 | } 14 | function createComment(text) { 15 | return document.createComment(text); 16 | } 17 | function insertBefore(parentNode, newNode, referenceNode) { 18 | parentNode.insertBefore(newNode, referenceNode); 19 | } 20 | function removeChild(node, child) { 21 | if (child.parentNode) { 22 | child.parentNode.removeChild(child); 23 | } 24 | } 25 | function appendChild(node, child) { 26 | node.appendChild(child); 27 | } 28 | function parentNode(node) { 29 | return node.parentNode; 30 | } 31 | function nextSibling(node) { 32 | return node.nextSibling; 33 | } 34 | function tagName(elm) { 35 | return elm.tagName; 36 | } 37 | function setTextContent(node, text) { 38 | node.textContent = text; 39 | } 40 | function getTextContent(node) { 41 | return node.textContent; 42 | } 43 | function isElement(node) { 44 | return node.nodeType === 1; 45 | } 46 | function isText(node) { 47 | return node.nodeType === 3; 48 | } 49 | function isComment(node) { 50 | return node.nodeType === 8; 51 | } 52 | const htmlDomApi = { 53 | createElement, 54 | createElementNS, 55 | createTextNode, 56 | createComment, 57 | insertBefore, 58 | removeChild, 59 | appendChild, 60 | parentNode, 61 | nextSibling, 62 | tagName, 63 | setTextContent, 64 | getTextContent, 65 | isElement, 66 | isText, 67 | isComment, 68 | }; 69 | 70 | function vnode(sel, data, children, text, elm) { 71 | const key = data === undefined ? undefined : data.key; 72 | return { sel, data, children, text, elm, key }; 73 | } 74 | 75 | const array = Array.isArray; 76 | function primitive(s) { 77 | return typeof s === "string" || typeof s === "number"; 78 | } 79 | 80 | function isUndef(s) { 81 | return s === undefined; 82 | } 83 | function isDef(s) { 84 | return s !== undefined; 85 | } 86 | const emptyNode = vnode("", {}, [], undefined, undefined); 87 | function sameVnode(vnode1, vnode2) { 88 | var _a, _b; 89 | const isSameKey = vnode1.key === vnode2.key; 90 | const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is); 91 | const isSameSel = vnode1.sel === vnode2.sel; 92 | return isSameSel && isSameKey && isSameIs; 93 | } 94 | function isVnode(vnode) { 95 | return vnode.sel !== undefined; 96 | } 97 | function createKeyToOldIdx(children, beginIdx, endIdx) { 98 | var _a; 99 | const map = {}; 100 | for (let i = beginIdx; i <= endIdx; ++i) { 101 | const key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key; 102 | if (key !== undefined) { 103 | map[key] = i; 104 | } 105 | } 106 | return map; 107 | } 108 | const hooks = [ 109 | "create", 110 | "update", 111 | "remove", 112 | "destroy", 113 | "pre", 114 | "post", 115 | ]; 116 | function init$1(modules, domApi) { 117 | let i; 118 | let j; 119 | const cbs = { 120 | create: [], 121 | update: [], 122 | remove: [], 123 | destroy: [], 124 | pre: [], 125 | post: [], 126 | }; 127 | const api = domApi !== undefined ? domApi : htmlDomApi; 128 | for (i = 0; i < hooks.length; ++i) { 129 | cbs[hooks[i]] = []; 130 | for (j = 0; j < modules.length; ++j) { 131 | const hook = modules[j][hooks[i]]; 132 | if (hook !== undefined) { 133 | cbs[hooks[i]].push(hook); 134 | } 135 | } 136 | } 137 | function emptyNodeAt(elm) { 138 | const id = elm.id ? "#" + elm.id : ""; 139 | // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot. 140 | // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring 141 | const classes = elm.getAttribute("class"); 142 | const c = classes ? "." + classes.split(" ").join(".") : ""; 143 | return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); 144 | } 145 | function createRmCb(childElm, listeners) { 146 | return function rmCb() { 147 | if (--listeners === 0) { 148 | const parent = api.parentNode(childElm); 149 | api.removeChild(parent, childElm); 150 | } 151 | }; 152 | } 153 | function createElm(vnode, insertedVnodeQueue) { 154 | var _a, _b; 155 | let i; 156 | let data = vnode.data; 157 | if (data !== undefined) { 158 | const init = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init; 159 | if (isDef(init)) { 160 | init(vnode); 161 | data = vnode.data; 162 | } 163 | } 164 | const children = vnode.children; 165 | const sel = vnode.sel; 166 | if (sel === "!") { 167 | if (isUndef(vnode.text)) { 168 | vnode.text = ""; 169 | } 170 | vnode.elm = api.createComment(vnode.text); 171 | } 172 | else if (sel !== undefined) { 173 | // Parse selector 174 | const hashIdx = sel.indexOf("#"); 175 | const dotIdx = sel.indexOf(".", hashIdx); 176 | const hash = hashIdx > 0 ? hashIdx : sel.length; 177 | const dot = dotIdx > 0 ? dotIdx : sel.length; 178 | const tag = hashIdx !== -1 || dotIdx !== -1 179 | ? sel.slice(0, Math.min(hash, dot)) 180 | : sel; 181 | const elm = (vnode.elm = 182 | isDef(data) && isDef((i = data.ns)) 183 | ? api.createElementNS(i, tag, data) 184 | : api.createElement(tag, data)); 185 | if (hash < dot) 186 | elm.setAttribute("id", sel.slice(hash + 1, dot)); 187 | if (dotIdx > 0) 188 | elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); 189 | for (i = 0; i < cbs.create.length; ++i) 190 | cbs.create[i](emptyNode, vnode); 191 | if (array(children)) { 192 | for (i = 0; i < children.length; ++i) { 193 | const ch = children[i]; 194 | if (ch != null) { 195 | api.appendChild(elm, createElm(ch, insertedVnodeQueue)); 196 | } 197 | } 198 | } 199 | else if (primitive(vnode.text)) { 200 | api.appendChild(elm, api.createTextNode(vnode.text)); 201 | } 202 | const hook = vnode.data.hook; 203 | if (isDef(hook)) { 204 | (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode); 205 | if (hook.insert) { 206 | insertedVnodeQueue.push(vnode); 207 | } 208 | } 209 | } 210 | else { 211 | vnode.elm = api.createTextNode(vnode.text); 212 | } 213 | return vnode.elm; 214 | } 215 | function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { 216 | for (; startIdx <= endIdx; ++startIdx) { 217 | const ch = vnodes[startIdx]; 218 | if (ch != null) { 219 | api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); 220 | } 221 | } 222 | } 223 | function invokeDestroyHook(vnode) { 224 | var _a, _b; 225 | const data = vnode.data; 226 | if (data !== undefined) { 227 | (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); 228 | for (let i = 0; i < cbs.destroy.length; ++i) 229 | cbs.destroy[i](vnode); 230 | if (vnode.children !== undefined) { 231 | for (let j = 0; j < vnode.children.length; ++j) { 232 | const child = vnode.children[j]; 233 | if (child != null && typeof child !== "string") { 234 | invokeDestroyHook(child); 235 | } 236 | } 237 | } 238 | } 239 | } 240 | function removeVnodes(parentElm, vnodes, startIdx, endIdx) { 241 | var _a, _b; 242 | for (; startIdx <= endIdx; ++startIdx) { 243 | let listeners; 244 | let rm; 245 | const ch = vnodes[startIdx]; 246 | if (ch != null) { 247 | if (isDef(ch.sel)) { 248 | invokeDestroyHook(ch); 249 | listeners = cbs.remove.length + 1; 250 | rm = createRmCb(ch.elm, listeners); 251 | for (let i = 0; i < cbs.remove.length; ++i) 252 | cbs.remove[i](ch, rm); 253 | const removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove; 254 | if (isDef(removeHook)) { 255 | removeHook(ch, rm); 256 | } 257 | else { 258 | rm(); 259 | } 260 | } 261 | else { 262 | // Text node 263 | api.removeChild(parentElm, ch.elm); 264 | } 265 | } 266 | } 267 | } 268 | function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { 269 | let oldStartIdx = 0; 270 | let newStartIdx = 0; 271 | let oldEndIdx = oldCh.length - 1; 272 | let oldStartVnode = oldCh[0]; 273 | let oldEndVnode = oldCh[oldEndIdx]; 274 | let newEndIdx = newCh.length - 1; 275 | let newStartVnode = newCh[0]; 276 | let newEndVnode = newCh[newEndIdx]; 277 | let oldKeyToIdx; 278 | let idxInOld; 279 | let elmToMove; 280 | let before; 281 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 282 | if (oldStartVnode == null) { 283 | oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left 284 | } 285 | else if (oldEndVnode == null) { 286 | oldEndVnode = oldCh[--oldEndIdx]; 287 | } 288 | else if (newStartVnode == null) { 289 | newStartVnode = newCh[++newStartIdx]; 290 | } 291 | else if (newEndVnode == null) { 292 | newEndVnode = newCh[--newEndIdx]; 293 | } 294 | else if (sameVnode(oldStartVnode, newStartVnode)) { 295 | patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); 296 | oldStartVnode = oldCh[++oldStartIdx]; 297 | newStartVnode = newCh[++newStartIdx]; 298 | } 299 | else if (sameVnode(oldEndVnode, newEndVnode)) { 300 | patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); 301 | oldEndVnode = oldCh[--oldEndIdx]; 302 | newEndVnode = newCh[--newEndIdx]; 303 | } 304 | else if (sameVnode(oldStartVnode, newEndVnode)) { 305 | // Vnode moved right 306 | patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); 307 | api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); 308 | oldStartVnode = oldCh[++oldStartIdx]; 309 | newEndVnode = newCh[--newEndIdx]; 310 | } 311 | else if (sameVnode(oldEndVnode, newStartVnode)) { 312 | // Vnode moved left 313 | patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); 314 | api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); 315 | oldEndVnode = oldCh[--oldEndIdx]; 316 | newStartVnode = newCh[++newStartIdx]; 317 | } 318 | else { 319 | if (oldKeyToIdx === undefined) { 320 | oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); 321 | } 322 | idxInOld = oldKeyToIdx[newStartVnode.key]; 323 | if (isUndef(idxInOld)) { 324 | // New element 325 | api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); 326 | } 327 | else { 328 | elmToMove = oldCh[idxInOld]; 329 | if (elmToMove.sel !== newStartVnode.sel) { 330 | api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); 331 | } 332 | else { 333 | patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); 334 | oldCh[idxInOld] = undefined; 335 | api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); 336 | } 337 | } 338 | newStartVnode = newCh[++newStartIdx]; 339 | } 340 | } 341 | if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { 342 | if (oldStartIdx > oldEndIdx) { 343 | before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; 344 | addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); 345 | } 346 | else { 347 | removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); 348 | } 349 | } 350 | } 351 | function patchVnode(oldVnode, vnode, insertedVnodeQueue) { 352 | var _a, _b, _c, _d, _e; 353 | const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook; 354 | (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); 355 | const elm = (vnode.elm = oldVnode.elm); 356 | const oldCh = oldVnode.children; 357 | const ch = vnode.children; 358 | if (oldVnode === vnode) 359 | return; 360 | if (vnode.data !== undefined) { 361 | for (let i = 0; i < cbs.update.length; ++i) 362 | cbs.update[i](oldVnode, vnode); 363 | (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode); 364 | } 365 | if (isUndef(vnode.text)) { 366 | if (isDef(oldCh) && isDef(ch)) { 367 | if (oldCh !== ch) 368 | updateChildren(elm, oldCh, ch, insertedVnodeQueue); 369 | } 370 | else if (isDef(ch)) { 371 | if (isDef(oldVnode.text)) 372 | api.setTextContent(elm, ""); 373 | addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); 374 | } 375 | else if (isDef(oldCh)) { 376 | removeVnodes(elm, oldCh, 0, oldCh.length - 1); 377 | } 378 | else if (isDef(oldVnode.text)) { 379 | api.setTextContent(elm, ""); 380 | } 381 | } 382 | else if (oldVnode.text !== vnode.text) { 383 | if (isDef(oldCh)) { 384 | removeVnodes(elm, oldCh, 0, oldCh.length - 1); 385 | } 386 | api.setTextContent(elm, vnode.text); 387 | } 388 | (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); 389 | } 390 | return function patch(oldVnode, vnode) { 391 | let i, elm, parent; 392 | const insertedVnodeQueue = []; 393 | for (i = 0; i < cbs.pre.length; ++i) 394 | cbs.pre[i](); 395 | if (!isVnode(oldVnode)) { 396 | oldVnode = emptyNodeAt(oldVnode); 397 | } 398 | if (sameVnode(oldVnode, vnode)) { 399 | patchVnode(oldVnode, vnode, insertedVnodeQueue); 400 | } 401 | else { 402 | elm = oldVnode.elm; 403 | parent = api.parentNode(elm); 404 | createElm(vnode, insertedVnodeQueue); 405 | if (parent !== null) { 406 | api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); 407 | removeVnodes(parent, [oldVnode], 0, 0); 408 | } 409 | } 410 | for (i = 0; i < insertedVnodeQueue.length; ++i) { 411 | insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); 412 | } 413 | for (i = 0; i < cbs.post.length; ++i) 414 | cbs.post[i](); 415 | return vnode; 416 | }; 417 | } 418 | 419 | function addNS(data, children, sel) { 420 | data.ns = "http://www.w3.org/2000/svg"; 421 | if (sel !== "foreignObject" && children !== undefined) { 422 | for (let i = 0; i < children.length; ++i) { 423 | const childData = children[i].data; 424 | if (childData !== undefined) { 425 | addNS(childData, children[i].children, children[i].sel); 426 | } 427 | } 428 | } 429 | } 430 | function h(sel, b, c) { 431 | let data = {}; 432 | let children; 433 | let text; 434 | let i; 435 | if (c !== undefined) { 436 | if (b !== null) { 437 | data = b; 438 | } 439 | if (array(c)) { 440 | children = c; 441 | } 442 | else if (primitive(c)) { 443 | text = c; 444 | } 445 | else if (c && c.sel) { 446 | children = [c]; 447 | } 448 | } 449 | else if (b !== undefined && b !== null) { 450 | if (array(b)) { 451 | children = b; 452 | } 453 | else if (primitive(b)) { 454 | text = b; 455 | } 456 | else if (b && b.sel) { 457 | children = [b]; 458 | } 459 | else { 460 | data = b; 461 | } 462 | } 463 | if (children !== undefined) { 464 | for (i = 0; i < children.length; ++i) { 465 | if (primitive(children[i])) 466 | children[i] = vnode(undefined, undefined, undefined, children[i], undefined); 467 | } 468 | } 469 | if (sel[0] === "s" && 470 | sel[1] === "v" && 471 | sel[2] === "g" && 472 | (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { 473 | addNS(data, children, sel); 474 | } 475 | return vnode(sel, data, children, text, undefined); 476 | } 477 | 478 | function copyToThunk(vnode, thunk) { 479 | vnode.data.fn = thunk.data.fn; 480 | vnode.data.args = thunk.data.args; 481 | thunk.data = vnode.data; 482 | thunk.children = vnode.children; 483 | thunk.text = vnode.text; 484 | thunk.elm = vnode.elm; 485 | } 486 | function init(thunk) { 487 | const cur = thunk.data; 488 | const vnode = cur.fn(...cur.args); 489 | copyToThunk(vnode, thunk); 490 | } 491 | function prepatch(oldVnode, thunk) { 492 | let i; 493 | const old = oldVnode.data; 494 | const cur = thunk.data; 495 | const oldArgs = old.args; 496 | const args = cur.args; 497 | if (old.fn !== cur.fn || oldArgs.length !== args.length) { 498 | copyToThunk(cur.fn(...args), thunk); 499 | return; 500 | } 501 | for (i = 0; i < args.length; ++i) { 502 | if (oldArgs[i] !== args[i]) { 503 | copyToThunk(cur.fn(...args), thunk); 504 | return; 505 | } 506 | } 507 | copyToThunk(oldVnode, thunk); 508 | } 509 | const thunk = function thunk(sel, key, fn, args) { 510 | if (args === undefined) { 511 | args = fn; 512 | fn = key; 513 | key = undefined; 514 | } 515 | return h(sel, { 516 | key: key, 517 | hook: { init, prepatch }, 518 | fn: fn, 519 | args: args, 520 | }); 521 | }; 522 | 523 | function pre(vnode, newVnode) { 524 | const attachData = vnode.data.attachData; 525 | // Copy created placeholder and real element from old vnode 526 | newVnode.data.attachData.placeholder = attachData.placeholder; 527 | newVnode.data.attachData.real = attachData.real; 528 | // Mount real element in vnode so the patch process operates on it 529 | vnode.elm = vnode.data.attachData.real; 530 | } 531 | function post(_, vnode) { 532 | // Mount dummy placeholder in vnode so potential reorders use it 533 | vnode.elm = vnode.data.attachData.placeholder; 534 | } 535 | function destroy(vnode) { 536 | // Remove placeholder 537 | if (vnode.elm !== undefined) { 538 | vnode.elm.parentNode.removeChild(vnode.elm); 539 | } 540 | // Remove real element from where it was inserted 541 | vnode.elm = vnode.data.attachData.real; 542 | } 543 | function create(_, vnode) { 544 | const real = vnode.elm; 545 | const attachData = vnode.data.attachData; 546 | const placeholder = document.createElement("span"); 547 | // Replace actual element with dummy placeholder 548 | // Snabbdom will then insert placeholder instead 549 | vnode.elm = placeholder; 550 | attachData.target.appendChild(real); 551 | attachData.real = real; 552 | attachData.placeholder = placeholder; 553 | } 554 | function attachTo(target, vnode) { 555 | if (vnode.data === undefined) 556 | vnode.data = {}; 557 | if (vnode.data.hook === undefined) 558 | vnode.data.hook = {}; 559 | const data = vnode.data; 560 | const hook = vnode.data.hook; 561 | data.attachData = { target: target, placeholder: undefined, real: undefined }; 562 | hook.create = create; 563 | hook.prepatch = pre; 564 | hook.postpatch = post; 565 | hook.destroy = destroy; 566 | return vnode; 567 | } 568 | 569 | function toVNode(node, domApi) { 570 | const api = domApi !== undefined ? domApi : htmlDomApi; 571 | let text; 572 | if (api.isElement(node)) { 573 | const id = node.id ? "#" + node.id : ""; 574 | const cn = node.getAttribute("class"); 575 | const c = cn ? "." + cn.split(" ").join(".") : ""; 576 | const sel = api.tagName(node).toLowerCase() + id + c; 577 | const attrs = {}; 578 | const children = []; 579 | let name; 580 | let i, n; 581 | const elmAttrs = node.attributes; 582 | const elmChildren = node.childNodes; 583 | for (i = 0, n = elmAttrs.length; i < n; i++) { 584 | name = elmAttrs[i].nodeName; 585 | if (name !== "id" && name !== "class") { 586 | attrs[name] = elmAttrs[i].nodeValue; 587 | } 588 | } 589 | for (i = 0, n = elmChildren.length; i < n; i++) { 590 | children.push(toVNode(elmChildren[i], domApi)); 591 | } 592 | return vnode(sel, { attrs }, children, undefined, node); 593 | } 594 | else if (api.isText(node)) { 595 | text = api.getTextContent(node); 596 | return vnode(undefined, undefined, undefined, text, node); 597 | } 598 | else if (api.isComment(node)) { 599 | text = api.getTextContent(node); 600 | return vnode("!", {}, [], text, node); 601 | } 602 | else { 603 | return vnode("", {}, [], undefined, node); 604 | } 605 | } 606 | 607 | const xlinkNS = "http://www.w3.org/1999/xlink"; 608 | const xmlNS = "http://www.w3.org/XML/1998/namespace"; 609 | const colonChar = 58; 610 | const xChar = 120; 611 | function updateAttrs(oldVnode, vnode) { 612 | let key; 613 | const elm = vnode.elm; 614 | let oldAttrs = oldVnode.data.attrs; 615 | let attrs = vnode.data.attrs; 616 | if (!oldAttrs && !attrs) 617 | return; 618 | if (oldAttrs === attrs) 619 | return; 620 | oldAttrs = oldAttrs || {}; 621 | attrs = attrs || {}; 622 | // update modified attributes, add new attributes 623 | for (key in attrs) { 624 | const cur = attrs[key]; 625 | const old = oldAttrs[key]; 626 | if (old !== cur) { 627 | if (cur === true) { 628 | elm.setAttribute(key, ""); 629 | } 630 | else if (cur === false) { 631 | elm.removeAttribute(key); 632 | } 633 | else { 634 | if (key.charCodeAt(0) !== xChar) { 635 | elm.setAttribute(key, cur); 636 | } 637 | else if (key.charCodeAt(3) === colonChar) { 638 | // Assume xml namespace 639 | elm.setAttributeNS(xmlNS, key, cur); 640 | } 641 | else if (key.charCodeAt(5) === colonChar) { 642 | // Assume xlink namespace 643 | elm.setAttributeNS(xlinkNS, key, cur); 644 | } 645 | else { 646 | elm.setAttribute(key, cur); 647 | } 648 | } 649 | } 650 | } 651 | // remove removed attributes 652 | // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) 653 | // the other option is to remove all attributes with value == undefined 654 | for (key in oldAttrs) { 655 | if (!(key in attrs)) { 656 | elm.removeAttribute(key); 657 | } 658 | } 659 | } 660 | const attributesModule = { 661 | create: updateAttrs, 662 | update: updateAttrs, 663 | }; 664 | 665 | function updateClass(oldVnode, vnode) { 666 | let cur; 667 | let name; 668 | const elm = vnode.elm; 669 | let oldClass = oldVnode.data.class; 670 | let klass = vnode.data.class; 671 | if (!oldClass && !klass) 672 | return; 673 | if (oldClass === klass) 674 | return; 675 | oldClass = oldClass || {}; 676 | klass = klass || {}; 677 | for (name in oldClass) { 678 | if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { 679 | // was `true` and now not provided 680 | elm.classList.remove(name); 681 | } 682 | } 683 | for (name in klass) { 684 | cur = klass[name]; 685 | if (cur !== oldClass[name]) { 686 | elm.classList[cur ? "add" : "remove"](name); 687 | } 688 | } 689 | } 690 | const classModule = { create: updateClass, update: updateClass }; 691 | 692 | const CAPS_REGEX = /[A-Z]/g; 693 | function updateDataset(oldVnode, vnode) { 694 | const elm = vnode.elm; 695 | let oldDataset = oldVnode.data.dataset; 696 | let dataset = vnode.data.dataset; 697 | let key; 698 | if (!oldDataset && !dataset) 699 | return; 700 | if (oldDataset === dataset) 701 | return; 702 | oldDataset = oldDataset || {}; 703 | dataset = dataset || {}; 704 | const d = elm.dataset; 705 | for (key in oldDataset) { 706 | if (!dataset[key]) { 707 | if (d) { 708 | if (key in d) { 709 | delete d[key]; 710 | } 711 | } 712 | else { 713 | elm.removeAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase()); 714 | } 715 | } 716 | } 717 | for (key in dataset) { 718 | if (oldDataset[key] !== dataset[key]) { 719 | if (d) { 720 | d[key] = dataset[key]; 721 | } 722 | else { 723 | elm.setAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase(), dataset[key]); 724 | } 725 | } 726 | } 727 | } 728 | const datasetModule = { 729 | create: updateDataset, 730 | update: updateDataset, 731 | }; 732 | 733 | function invokeHandler(handler, vnode, event) { 734 | if (typeof handler === "function") { 735 | // call function handler 736 | handler.call(vnode, event, vnode); 737 | } 738 | else if (typeof handler === "object") { 739 | // call multiple handlers 740 | for (let i = 0; i < handler.length; i++) { 741 | invokeHandler(handler[i], vnode, event); 742 | } 743 | } 744 | } 745 | function handleEvent(event, vnode) { 746 | const name = event.type; 747 | const on = vnode.data.on; 748 | // call event handler(s) if exists 749 | if (on && on[name]) { 750 | invokeHandler(on[name], vnode, event); 751 | } 752 | } 753 | function createListener() { 754 | return function handler(event) { 755 | handleEvent(event, handler.vnode); 756 | }; 757 | } 758 | function updateEventListeners(oldVnode, vnode) { 759 | const oldOn = oldVnode.data.on; 760 | const oldListener = oldVnode.listener; 761 | const oldElm = oldVnode.elm; 762 | const on = vnode && vnode.data.on; 763 | const elm = (vnode && vnode.elm); 764 | let name; 765 | // optimization for reused immutable handlers 766 | if (oldOn === on) { 767 | return; 768 | } 769 | // remove existing listeners which no longer used 770 | if (oldOn && oldListener) { 771 | // if element changed or deleted we remove all existing listeners unconditionally 772 | if (!on) { 773 | for (name in oldOn) { 774 | // remove listener if element was changed or existing listeners removed 775 | oldElm.removeEventListener(name, oldListener, false); 776 | } 777 | } 778 | else { 779 | for (name in oldOn) { 780 | // remove listener if existing listener removed 781 | if (!on[name]) { 782 | oldElm.removeEventListener(name, oldListener, false); 783 | } 784 | } 785 | } 786 | } 787 | // add new listeners which has not already attached 788 | if (on) { 789 | // reuse existing listener or create new 790 | const listener = (vnode.listener = 791 | oldVnode.listener || createListener()); 792 | // update vnode for listener 793 | listener.vnode = vnode; 794 | // if element changed or added we add all needed listeners unconditionally 795 | if (!oldOn) { 796 | for (name in on) { 797 | // add listener if element was changed or new listeners added 798 | elm.addEventListener(name, listener, false); 799 | } 800 | } 801 | else { 802 | for (name in on) { 803 | // add listener if new listener added 804 | if (!oldOn[name]) { 805 | elm.addEventListener(name, listener, false); 806 | } 807 | } 808 | } 809 | } 810 | } 811 | const eventListenersModule = { 812 | create: updateEventListeners, 813 | update: updateEventListeners, 814 | destroy: updateEventListeners, 815 | }; 816 | 817 | function updateProps(oldVnode, vnode) { 818 | let key; 819 | let cur; 820 | let old; 821 | const elm = vnode.elm; 822 | let oldProps = oldVnode.data.props; 823 | let props = vnode.data.props; 824 | if (!oldProps && !props) 825 | return; 826 | if (oldProps === props) 827 | return; 828 | oldProps = oldProps || {}; 829 | props = props || {}; 830 | for (key in props) { 831 | cur = props[key]; 832 | old = oldProps[key]; 833 | if (old !== cur && (key !== "value" || elm[key] !== cur)) { 834 | elm[key] = cur; 835 | } 836 | } 837 | } 838 | const propsModule = { create: updateProps, update: updateProps }; 839 | 840 | // Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409. 841 | const raf = (typeof window !== "undefined" && 842 | window.requestAnimationFrame.bind(window)) || 843 | setTimeout; 844 | const nextFrame = function (fn) { 845 | raf(function () { 846 | raf(fn); 847 | }); 848 | }; 849 | let reflowForced = false; 850 | function setNextFrame(obj, prop, val) { 851 | nextFrame(function () { 852 | obj[prop] = val; 853 | }); 854 | } 855 | function updateStyle(oldVnode, vnode) { 856 | let cur; 857 | let name; 858 | const elm = vnode.elm; 859 | let oldStyle = oldVnode.data.style; 860 | let style = vnode.data.style; 861 | if (!oldStyle && !style) 862 | return; 863 | if (oldStyle === style) 864 | return; 865 | oldStyle = oldStyle || {}; 866 | style = style || {}; 867 | const oldHasDel = "delayed" in oldStyle; 868 | for (name in oldStyle) { 869 | if (!style[name]) { 870 | if (name[0] === "-" && name[1] === "-") { 871 | elm.style.removeProperty(name); 872 | } 873 | else { 874 | elm.style[name] = ""; 875 | } 876 | } 877 | } 878 | for (name in style) { 879 | cur = style[name]; 880 | if (name === "delayed" && style.delayed) { 881 | for (const name2 in style.delayed) { 882 | cur = style.delayed[name2]; 883 | if (!oldHasDel || cur !== oldStyle.delayed[name2]) { 884 | setNextFrame(elm.style, name2, cur); 885 | } 886 | } 887 | } 888 | else if (name !== "remove" && cur !== oldStyle[name]) { 889 | if (name[0] === "-" && name[1] === "-") { 890 | elm.style.setProperty(name, cur); 891 | } 892 | else { 893 | elm.style[name] = cur; 894 | } 895 | } 896 | } 897 | } 898 | function applyDestroyStyle(vnode) { 899 | let style; 900 | let name; 901 | const elm = vnode.elm; 902 | const s = vnode.data.style; 903 | if (!s || !(style = s.destroy)) 904 | return; 905 | for (name in style) { 906 | elm.style[name] = style[name]; 907 | } 908 | } 909 | function applyRemoveStyle(vnode, rm) { 910 | const s = vnode.data.style; 911 | if (!s || !s.remove) { 912 | rm(); 913 | return; 914 | } 915 | if (!reflowForced) { 916 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 917 | vnode.elm.offsetLeft; 918 | reflowForced = true; 919 | } 920 | let name; 921 | const elm = vnode.elm; 922 | let i = 0; 923 | const style = s.remove; 924 | let amount = 0; 925 | const applied = []; 926 | for (name in style) { 927 | applied.push(name); 928 | elm.style[name] = style[name]; 929 | } 930 | const compStyle = getComputedStyle(elm); 931 | const props = compStyle["transition-property"].split(", "); 932 | for (; i < props.length; ++i) { 933 | if (applied.indexOf(props[i]) !== -1) 934 | amount++; 935 | } 936 | elm.addEventListener("transitionend", function (ev) { 937 | if (ev.target === elm) 938 | --amount; 939 | if (amount === 0) 940 | rm(); 941 | }); 942 | } 943 | function forceReflow() { 944 | reflowForced = false; 945 | } 946 | const styleModule = { 947 | pre: forceReflow, 948 | create: updateStyle, 949 | update: updateStyle, 950 | destroy: applyDestroyStyle, 951 | remove: applyRemoveStyle, 952 | }; 953 | 954 | /* eslint-disable @typescript-eslint/no-namespace, import/export */ 955 | function flattenAndFilter(children, flattened) { 956 | for (const child of children) { 957 | // filter out falsey children, except 0 since zero can be a valid value e.g inside a chart 958 | if (child !== undefined && 959 | child !== null && 960 | child !== false && 961 | child !== "") { 962 | if (Array.isArray(child)) { 963 | flattenAndFilter(child, flattened); 964 | } 965 | else if (typeof child === "string" || 966 | typeof child === "number" || 967 | typeof child === "boolean") { 968 | flattened.push(vnode(undefined, undefined, undefined, String(child), undefined)); 969 | } 970 | else { 971 | flattened.push(child); 972 | } 973 | } 974 | } 975 | return flattened; 976 | } 977 | /** 978 | * jsx/tsx compatible factory function 979 | * see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions 980 | */ 981 | function jsx(tag, data, ...children) { 982 | const flatChildren = flattenAndFilter(children, []); 983 | if (typeof tag === "function") { 984 | // tag is a function component 985 | return tag(data, flatChildren); 986 | } 987 | else { 988 | if (flatChildren.length === 1 && 989 | !flatChildren[0].sel && 990 | flatChildren[0].text) { 991 | // only child is a simple text node, pass as text for a simpler vtree 992 | return h(tag, data, flatChildren[0].text); 993 | } 994 | else { 995 | return h(tag, data, flatChildren); 996 | } 997 | } 998 | } 999 | (function (jsx) { 1000 | })(jsx || (jsx = {})); 1001 | 1002 | snabbdom.array = array; 1003 | snabbdom.attachTo = attachTo; 1004 | snabbdom.attributesModule = attributesModule; 1005 | snabbdom.classModule = classModule; 1006 | snabbdom.datasetModule = datasetModule; 1007 | snabbdom.eventListenersModule = eventListenersModule; 1008 | snabbdom.h = h; 1009 | snabbdom.htmlDomApi = htmlDomApi; 1010 | snabbdom.init = init$1; 1011 | snabbdom.jsx = jsx; 1012 | snabbdom.primitive = primitive; 1013 | snabbdom.propsModule = propsModule; 1014 | snabbdom.styleModule = styleModule; 1015 | snabbdom.thunk = thunk; 1016 | snabbdom.toVNode = toVNode; 1017 | snabbdom.vnode = vnode; 1018 | -------------------------------------------------------------------------------- /test/dumdom/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns dumdom.core-test 2 | (:require [cljs.test :as t :refer-macros [async is testing]] 3 | [devcards.core :refer-macros [deftest]] 4 | [dumdom.core :as sut] 5 | [dumdom.component] 6 | [dumdom.dom :as d] 7 | [dumdom.test-helper :refer [render render-str]])) 8 | 9 | (sut/purge!) 10 | 11 | (deftest component-render 12 | (testing "Renders component" 13 | (let [comp (sut/component (fn [data] (d/div {:className "lol"} data)))] 14 | (is (= "
    1
    " (render-str (comp 1)))))) 15 | 16 | (testing "Does not optimize away initial render when data is nil" 17 | (let [comp (sut/component (fn [data] (d/div {:className "lol"})))] 18 | (is (= "
    " (render-str (comp nil)))))) 19 | 20 | (testing "Does not re-render when immutable value hasn't changed" 21 | (let [mutable-state (atom [1 2 3]) 22 | comp (sut/component (fn [data] 23 | (let [v (first @mutable-state)] 24 | (swap! mutable-state rest) 25 | (d/div {} v))))] 26 | (is (= "
    1
    " (render-str (comp {:number 1})))) 27 | (is (= "
    1
    " (render-str (comp {:number 1})))) 28 | (is (= "
    2
    " (render-str (comp {:number 2})))))) 29 | 30 | (testing "Re-renders instance of component at different position in tree" 31 | (let [mutable-state (atom [1 2 3]) 32 | comp (sut/component (fn [data] 33 | (let [v (first @mutable-state)] 34 | (swap! mutable-state rest) 35 | (d/div {} v))))] 36 | (is (= "
    1
    " (render-str (comp {:number 1}) [] 0))) 37 | (is (= "
    2
    " (render-str (comp {:number 1}) [] 1))) 38 | (is (= "
    1
    " (render-str (comp {:number 1}) [] 0))) 39 | (is (= "
    2
    " (render-str (comp {:number 1}) [] 1))) 40 | (is (= "
    3
    " (render-str (comp {:number 2}) [] 1))))) 41 | 42 | (testing "Ignores provided position when component has a keyfn" 43 | (let [mutable-state (atom [1 2 3]) 44 | comp (sut/component (fn [data] 45 | (let [v (first @mutable-state)] 46 | (swap! mutable-state rest) 47 | (d/div {} v))) 48 | {:keyfn :id})] 49 | (is (= "
    1
    " (render-str (comp {:id "c1" :number 1}) [] 0))) 50 | (is (= "
    1
    " (render-str (comp {:id "c1" :number 1}) [] 1))) 51 | (is (= "
    2
    " (render-str (comp {:id "c2" :number 1}) [] 0))) 52 | (is (= "
    3
    " (render-str (comp {:number 1}) [] 0))) 53 | (is (= "
    2
    " (render-str (comp {:id "c2" :number 1}) [] 1))))) 54 | 55 | (testing "Sets key on vdom node" 56 | (let [comp (sut/component (fn [data] 57 | (d/div {} (:val data))) 58 | {:keyfn :id})] 59 | (is (= "c1" (:key (render (comp {:id "c1" :val 1}))))))) 60 | 61 | (testing "keyfn overrides vdom node key" 62 | (let [comp (sut/component (fn [data] 63 | (d/div {:key "key"} (:val data))) 64 | {:keyfn :id})] 65 | (is (= "c1" (:key (render (comp {:id "c1" :val 1}))))))) 66 | 67 | (testing "Passes constant args to component, but does not re-render when they change" 68 | (let [calls (atom []) 69 | comp (sut/component (fn [& args] 70 | (swap! calls conj args) 71 | (d/div {:key "key"} "OK")))] 72 | (render (comp {:id "v1"} 1 2 3)) 73 | (render (comp {:id "v1"} 2 3 4)) 74 | (render (comp {:id "v2"} 3 4 5)) 75 | (render (comp {:id "v2"} 4 5 6)) 76 | (render (comp {:id "v3"} 5 6 7)) 77 | (is (= [[{:id "v1"} 1 2 3] 78 | [{:id "v2"} 3 4 5] 79 | [{:id "v3"} 5 6 7]] @calls)))) 80 | 81 | (testing "Renders component as child of DOM element" 82 | (let [comp (sut/component (fn [data] 83 | (d/p {} "From component")))] 84 | (is (= "

    From component

    " 85 | (render-str (d/div {:className "wrapper"} 86 | (comp {:id "v1"} 1 2 3))))))) 87 | 88 | (testing "Does not freak out when rendering nil children" 89 | (let [comp (sut/component (fn [data] 90 | (d/div {} 91 | (d/h1 {} "Hello") 92 | nil 93 | (d/p {} "Ok"))))] 94 | (is (= "

    Hello

    Ok

    Meh
    " 95 | (render-str (d/div {:className "wrapper"} 96 | (comp {}) 97 | nil 98 | (d/div {} "Meh"))))))) 99 | 100 | (testing "Allows components to return nil" 101 | (let [comp (sut/component (fn [data] nil))] 102 | (is (= "
    Meh
    " 103 | (render-str (d/div {:className "wrapper"} 104 | (comp {}) 105 | (d/div {} "Meh")))))))) 106 | 107 | (deftest render-test 108 | (testing "Renders vnode to DOM" 109 | (let [el (js/document.createElement "div")] 110 | (sut/render (d/div {} "Hello") el) 111 | (is (= "
    Hello
    " (.-innerHTML el))))) 112 | 113 | (testing "Renders hiccup" 114 | (let [el (js/document.createElement "div")] 115 | (sut/render [:div {} 116 | [:h1 {:style {:border "1px solid cyan"}} "Hello"] 117 | [:img {:border "2"}]] el) 118 | (is (= "

    Hello

    " (.-innerHTML el))))) 119 | 120 | (testing "Renders hiccup with comment placeholders for nils" 121 | (let [el (js/document.createElement "div")] 122 | (sut/render [:div {} 123 | nil 124 | [:img {:border "2"}]] el) 125 | (is (= "
    " (.-innerHTML el))))) 126 | 127 | (testing "Renders custom elements in hiccup" 128 | (let [el (js/document.createElement "div") 129 | comp (sut/component (fn [data] 130 | (d/a {} "Hi there " (:name data))))] 131 | (sut/render [comp {:name "world"}] el) 132 | (is (= "Hi there world" (.-innerHTML el))))) 133 | 134 | (testing "Allows custom elements to return hiccup" 135 | (let [el (js/document.createElement "div") 136 | comp (sut/component (fn [data] 137 | [:a "Hi there " (:name data)]))] 138 | (sut/render [comp {:name "world"}] el) 139 | (is (= "Hi there world" (.-innerHTML el))))) 140 | 141 | (testing "Renders hiccup with a list of children" 142 | (let [el (js/document.createElement "div")] 143 | (sut/render [:ul (list [:li "One"] [:li "two"] [:li "three"])] el) 144 | (is (= "
    • One
    • two
    • three
    " (.-innerHTML el))))) 145 | 146 | (testing "Allows components to return nil on first render" 147 | (let [el (js/document.createElement "div") 148 | comp (sut/component (fn [data] nil))] 149 | (sut/render [comp {}] el) 150 | (is (= "" (.-innerHTML el))))) 151 | 152 | (testing "Allows components to return nil, then something" 153 | (let [el (js/document.createElement "div") 154 | comp (sut/component (fn [{:keys [text]}] (if text [:p text] nil)))] 155 | (sut/render [comp {}] el) 156 | (sut/render [comp {:text "#1"}] el) 157 | (sut/render [comp {:text "#2"}] el) 158 | (is (= "

    #2

    " (.-innerHTML el))))) 159 | 160 | (testing "Allows components to return something, nil" 161 | (let [el (js/document.createElement "div") 162 | comp (sut/component (fn [{:keys [text]}] (if text [:p text] nil)))] 163 | (sut/render [comp {:text "#1"}] el) 164 | (sut/render [comp {}] el) 165 | (is (= "" (.-innerHTML el))))) 166 | 167 | (testing "Allows components to return something, nil, something" 168 | (let [el (js/document.createElement "div") 169 | comp (sut/component (fn [{:keys [text]}] (if text [:p text] nil)))] 170 | (sut/render [comp {:text "#1"}] el) 171 | (sut/render [comp {}] el) 172 | (sut/render [comp {:text "#2"}] el) 173 | (is (= "

    #2

    " (.-innerHTML el)))))) 174 | 175 | (deftest on-mount-test 176 | (testing "Calls on-mount when component first mounts" 177 | (let [el (js/document.createElement "div") 178 | on-mount (atom nil) 179 | component (sut/component 180 | (fn [_] (d/div {} "LOL")) 181 | {:on-mount (fn [node & args] 182 | (reset! on-mount (apply vector node args)))})] 183 | (sut/render (component {:a 42} {:static "Prop"} {:another "Static"}) el) 184 | (is (= [(.-firstChild el) {:a 42} {:static "Prop"} {:another "Static"}] 185 | @on-mount)))) 186 | 187 | (testing "Calls both on-mount and ref callback" 188 | (let [el (js/document.createElement "div") 189 | on-mount (atom nil) 190 | ref (atom nil) 191 | component (sut/component 192 | (fn [data] (d/div {:ref #(reset! ref data)} (:text data))) 193 | {:on-mount #(reset! on-mount %2)})] 194 | (sut/render (component {:text "Look ma!"}) el) 195 | (is (= {:text "Look ma!"} @on-mount)) 196 | (is (= {:text "Look ma!"} @ref)))) 197 | 198 | (testing "Does not call on-mount on update" 199 | (let [el (js/document.createElement "div") 200 | on-mount (atom []) 201 | component (sut/component 202 | (fn [data] (d/div {} (:text data))) 203 | {:on-mount (fn [node data] 204 | (swap! on-mount conj data))})] 205 | (sut/render (component {:text "LOL"}) el) 206 | (sut/render (component {:text "Hello"}) el) 207 | (is (= 1 (count @on-mount))))) 208 | 209 | (testing "Does not call on-mount when keyed component changes position" 210 | (let [el (js/document.createElement "div") 211 | on-mount (atom []) 212 | component (sut/component 213 | (fn [data] (d/div {} (:text data))) 214 | {:on-mount (fn [node data] 215 | (swap! on-mount conj data)) 216 | :keyfn #(str "key")})] 217 | (sut/render (d/div {} (component {:text "LOL"})) el) 218 | (sut/render (d/div {} (d/div {} "Look:") (component {:text "Hello"})) el) 219 | (is (= 1 (count @on-mount)))))) 220 | 221 | (deftest on-update-test 222 | (testing "Does not call on-update when component first mounts" 223 | (let [el (js/document.createElement "div") 224 | on-update (atom nil) 225 | component (sut/component 226 | (fn [_] (d/div {} "LOL")) 227 | {:on-update (fn [node & args] 228 | (reset! on-update (apply vector node args)))})] 229 | (sut/render (component {:a 42} {:static "Prop"} {:another "Static"}) el) 230 | (is (nil? @on-update)))) 231 | 232 | (testing "Calls on-update on each update" 233 | (let [el (js/document.createElement "div") 234 | on-update (atom []) 235 | component (sut/component 236 | (fn [data] (d/div {} (:text data))) 237 | {:on-update (fn [node data] 238 | (swap! on-update conj data))})] 239 | (sut/render (component {:text "LOL"}) el) 240 | (sut/render (component {:text "Hello"}) el) 241 | (sut/render (component {:text "Aight"}) el) 242 | (is (= [{:text "Hello"} {:text "Aight"}] @on-update))))) 243 | 244 | (deftest on-render-test 245 | (testing "Calls on-render when component first mounts" 246 | (let [el (js/document.createElement "div") 247 | on-render (atom nil) 248 | component (sut/component 249 | (fn [_] (d/div {} "LOL")) 250 | {:on-render (fn [node & args] 251 | (reset! on-render (apply vector node args)))})] 252 | (sut/render (component {:a 42} {:static "Prop"} {:another "Static"}) el) 253 | (is (= [(.-firstChild el) {:a 42} nil {:static "Prop"} {:another "Static"}] 254 | @on-render)))) 255 | 256 | (testing "Calls on-render and on-mount when component first mounts" 257 | (let [el (js/document.createElement "div") 258 | on-render (atom nil) 259 | on-mount (atom nil) 260 | component (sut/component 261 | (fn [_] (d/div {} "LOL")) 262 | {:on-render (fn [node & args] 263 | (reset! on-render (apply vector node args))) 264 | :on-mount (fn [node & args] 265 | (reset! on-mount (apply vector node args)))})] 266 | (sut/render (component {:a 42} {:static "Prop"} {:another "Static"}) el) 267 | (is (= [(.-firstChild el) {:a 42} nil {:static "Prop"} {:another "Static"}] 268 | @on-render)) 269 | (is (= [(.-firstChild el) {:a 42} {:static "Prop"} {:another "Static"}] 270 | @on-mount)))) 271 | 272 | (testing "Calls on-render on each update" 273 | (let [el (js/document.createElement "div") 274 | on-render (atom []) 275 | component (sut/component 276 | (fn [data] (d/div {} (:text data))) 277 | {:on-render (fn [node data] 278 | (swap! on-render conj data))})] 279 | (sut/render (component {:text "LOL"}) el) 280 | (sut/render (component {:text "Hello"}) el) 281 | (sut/render (component {:text "Aight"}) el) 282 | (is (= [{:text "LOL"} 283 | {:text "Hello"} 284 | {:text "Aight"}] @on-render)))) 285 | 286 | (testing "Calls on-render and on-update on each update" 287 | (let [el (js/document.createElement "div") 288 | on-render (atom []) 289 | on-update (atom []) 290 | component (sut/component 291 | (fn [data] (d/div {} (:text data))) 292 | {:on-render (fn [node data] 293 | (swap! on-render conj data)) 294 | :on-update (fn [node data] 295 | (swap! on-update conj data))})] 296 | (sut/render (component {:text "LOL"}) el) 297 | (sut/render (component {:text "Hello"}) el) 298 | (sut/render (component {:text "Aight"}) el) 299 | (is (= [{:text "LOL"} 300 | {:text "Hello"} 301 | {:text "Aight"}] @on-render)) 302 | (is (= [{:text "Hello"} 303 | {:text "Aight"}] @on-update)))) 304 | 305 | (testing "Passes previous data to on-render" 306 | (let [el (js/document.createElement "div") 307 | on-render (atom []) 308 | component (sut/component 309 | (fn [data] (d/div {} (:text data))) 310 | {:on-render (fn [node data old-data statics] 311 | (swap! on-render conj [data old-data statics]))})] 312 | (sut/render (component {:text "LOL"} 2) el) 313 | (sut/render (component {:text "Hello"} 2) el) 314 | (sut/render (component {:text "Aight"} 2) el) 315 | (is (= [[{:text "LOL"} nil 2] 316 | [{:text "Hello"} {:text "LOL"} 2] 317 | [{:text "Aight"} {:text "Hello"} 2]] @on-render)))) 318 | 319 | (testing "Passes previous data to on-update" 320 | (let [el (js/document.createElement "div") 321 | on-update (atom []) 322 | component (sut/component 323 | (fn [data] (d/div {} (:text data))) 324 | {:on-update (fn [node data old-data statics] 325 | (swap! on-update conj [data old-data statics]))})] 326 | (sut/render (component {:text "LOL"} 2) el) 327 | (sut/render (component {:text "Hello"} 2) el) 328 | (sut/render (component {:text "Aight"} 2) el) 329 | (is (= [[{:text "Hello"} {:text "LOL"} 2] 330 | [{:text "Aight"} {:text "Hello"} 2]] @on-update))))) 331 | 332 | (deftest on-unmount-test 333 | (testing "Does not call on-unmount when component first mounts" 334 | (let [el (js/document.createElement "div") 335 | on-unmount (atom nil) 336 | component (sut/component 337 | (fn [_] (d/div {} "LOL")) 338 | {:on-unmount #(reset! on-unmount %)})] 339 | (sut/render (component {:a 42}) el) 340 | (is (nil? @on-unmount)))) 341 | 342 | (testing "Does not call on-unmount when updating component" 343 | (let [el (js/document.createElement "div") 344 | on-unmount (atom nil) 345 | component (sut/component 346 | (fn [_] (d/div {} "LOL")) 347 | {:on-unmount #(reset! on-unmount %)})] 348 | (sut/render (component {:a 42}) el) 349 | (sut/render (component {:a 13}) el) 350 | (is (nil? @on-unmount)))) 351 | 352 | (testing "Calls on-unmount when removing component" 353 | (let [el (js/document.createElement "div") 354 | on-unmount (atom nil) 355 | component (sut/component 356 | (fn [data] (d/div {} (:text data))) 357 | {:on-unmount (fn [node & args] 358 | (reset! on-unmount (apply vector node args)))})] 359 | (sut/render (component {:text "LOL"}) el) 360 | (let [rendered (.-firstChild el)] 361 | (sut/render (d/h1 {} "Gone!") el) 362 | (is (= [rendered {:text "LOL"}] @on-unmount)))))) 363 | 364 | (deftest animation-callbacks-test 365 | (testing "Calls will-appear when parent mounts" 366 | (let [el (js/document.createElement "div") 367 | will-appear (atom nil) 368 | component (sut/component 369 | (fn [data] (d/div {} (:text data))) 370 | {:will-appear (fn [node callback & args] 371 | (reset! will-appear (apply vector node args)))})] 372 | (sut/render (d/div {} (component {:text "LOL"})) el) 373 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @will-appear)))) 374 | 375 | (testing "Calls did-appear when the callback passed to will-appear is called" 376 | (let [el (js/document.createElement "div") 377 | will-appear (atom nil) 378 | did-appear (atom nil) 379 | component (sut/component 380 | (fn [data] (d/div {} (:text data))) 381 | {:will-appear (fn [node callback & args] (reset! will-appear callback)) 382 | :did-appear (fn [node & args] 383 | (reset! did-appear (apply vector node args)))})] 384 | (sut/render (d/div {} (component {:text "LOL"})) el) 385 | (is (nil? @did-appear)) 386 | (@will-appear) 387 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @did-appear)))) 388 | 389 | (testing "Does not call will-appear when parent updates" 390 | (let [el (js/document.createElement "div") 391 | will-appear (atom 0) 392 | component (sut/component 393 | (fn [data] (d/div {} (:text data))) 394 | {:will-appear #(swap! will-appear inc)})] 395 | (sut/render (d/div {} (component {:text "LOL"})) el) 396 | (sut/render (d/div {} (component {:text "LOL!!"})) el) 397 | (is (= 1 @will-appear)))) 398 | 399 | (testing "Does not call will-enter when parent mounts" 400 | (let [el (js/document.createElement "div") 401 | will-enter (atom nil) 402 | component (sut/component 403 | (fn [data] (d/div {} (:text data))) 404 | {:will-enter (fn [node & args] 405 | (reset! will-enter (apply vector node args)))})] 406 | (sut/render (d/div {} (component {:text "LOL"})) el) 407 | (is (nil? @will-enter)))) 408 | 409 | (testing "Calls will-enter when mounting element inside existing parent" 410 | (let [el (js/document.createElement "div") 411 | will-enter (atom nil) 412 | component (sut/component 413 | (fn [data] (d/div {} (:text data))) 414 | {:will-enter (fn [node callback & args] 415 | (reset! will-enter (apply vector node args)))})] 416 | (sut/render (d/div {}) el) 417 | (sut/render (d/div {} (component {:text "LOL"})) el) 418 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @will-enter)))) 419 | 420 | (testing "Does not call will-enter when moving element inside existing parent" 421 | (let [el (js/document.createElement "div") 422 | will-enter (atom 0) 423 | component (sut/component 424 | (fn [data] (d/div {} (:text data))) 425 | {:will-enter #(swap! will-enter inc) 426 | :keyfn #(str "comp")})] 427 | (sut/render (d/div {}) el) 428 | (sut/render (d/div {} (component {:text "LOL"})) el) 429 | (sut/render (d/div {} (d/div {} "Hmm") (component {:text "LOL"})) el) 430 | (is (= 1 @will-enter)))) 431 | 432 | (testing "Calls did-enter when the callback passed to will-enter is called" 433 | (let [el (js/document.createElement "div") 434 | will-enter (atom nil) 435 | did-enter (atom nil) 436 | component (sut/component 437 | (fn [data] (d/div {} (:text data))) 438 | {:will-enter (fn [node callback & args] (reset! will-enter callback)) 439 | :did-enter (fn [node & args] 440 | (reset! did-enter (apply vector node args)))})] 441 | (sut/render (d/div {}) el) 442 | (sut/render (d/div {} (component {:text "LOL"})) el) 443 | (is (nil? @did-enter)) 444 | (@will-enter) 445 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @did-enter)))) 446 | 447 | (testing "Calls the will-leave callback when unmounting the DOM node" 448 | (let [el (js/document.createElement "div") 449 | will-leave (atom nil) 450 | component (sut/component 451 | (fn [data] (d/div {} (:text data))) 452 | {:will-leave (fn [node callback & args] 453 | (reset! will-leave (apply vector node args)))})] 454 | (sut/render (d/div {} (component {:text "LOL"})) el) 455 | (sut/render (d/div {}) el) 456 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @will-leave)))) 457 | 458 | (testing "Does not call will-leave until enter callback is called" 459 | (let [el (js/document.createElement "div") 460 | will-enter (atom nil) 461 | will-leave (atom nil) 462 | component (sut/component 463 | (fn [data] (d/div {} (:text data))) 464 | {:will-enter (fn [node callback] (reset! will-enter callback)) 465 | :will-leave (fn [node callback & args] 466 | (reset! will-leave (apply vector node args)))})] 467 | (sut/render (d/div {}) el) 468 | (sut/render (d/div {} (component {:text "LOL"})) el) 469 | (sut/render (d/div {}) el) 470 | (is (nil? @will-leave)) 471 | (@will-enter) 472 | (is (= [(.. el -firstChild -firstChild) {:text "LOL"}] @will-leave)))) 473 | 474 | (testing "Calls did-leave when the callback passed to will-leave is called" 475 | (let [el (js/document.createElement "div") 476 | will-leave (atom nil) 477 | did-leave (atom nil) 478 | component (sut/component 479 | (fn [data] (d/div {} (:text data))) 480 | {:will-leave (fn [node callback & args] (reset! will-leave callback)) 481 | :did-leave (fn [node & args] 482 | (reset! did-leave (apply vector node args)))})] 483 | (sut/render (d/div {} (component {:text "LOL"})) el) 484 | (sut/render (d/div {}) el) 485 | (let [element (.. el -firstChild -firstChild)] 486 | (is (nil? @did-leave)) 487 | (@will-leave) 488 | (is (= [element {:text "LOL"}] @did-leave))))) 489 | 490 | (testing "Does not call will-leave when parent element is removed" 491 | (let [el (js/document.createElement "div") 492 | will-leave (atom nil) 493 | component (sut/component 494 | (fn [data] (d/div {} (:text data))) 495 | {:will-leave (fn [node callback & args] (reset! will-leave callback))})] 496 | (sut/render (d/div {} (d/div {} (component {:text "LOL"}))) el) 497 | (sut/render (d/div {}) el) 498 | (is (nil? @will-leave))))) 499 | 500 | (deftest TransitionGroup-test 501 | (testing "TransitionGroup creates span component" 502 | (let [el (js/document.createElement "div")] 503 | (sut/render (sut/TransitionGroup {} []) el) 504 | (is (= (.-innerHTML el) "")))) 505 | 506 | (testing "Creates TransitionGroup with custom component tag" 507 | (let [el (js/document.createElement "div")] 508 | (sut/render (sut/TransitionGroup {:component "div"} []) el) 509 | (is (= (.-innerHTML el) "
    ")))) 510 | 511 | (testing "Creates TransitionGroup with custom attributes" 512 | (let [el (js/document.createElement "div")] 513 | (sut/render (sut/TransitionGroup {:component "div" :className "lol" :id "ok"} []) el) 514 | (is (= (.. el -firstChild -className) "lol")) 515 | (is (= (.. el -firstChild -id) "ok")))) 516 | 517 | (testing "Creates TransitionGroup with custom component" 518 | (let [el (js/document.createElement "div") 519 | component (sut/component 520 | (fn [children] 521 | (d/h1 {} children)))] 522 | (sut/render (sut/TransitionGroup {:component component} 523 | [(d/span {} "Hey") (d/span {} "there!")]) el) 524 | (is (= (.-innerHTML el) "

    Heythere!

    "))))) 525 | 526 | (deftest CSSTransitionGroupEnterTest 527 | (testing "Adds enter class name according to the transition name" 528 | (let [el (js/document.createElement "div")] 529 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 530 | (sut/render 531 | (sut/CSSTransitionGroup 532 | {:transitionName "example"} 533 | [(d/div {:key "#1"} "I will enter")]) 534 | el) 535 | (is (= "example-enter" (.. el -firstChild -firstChild -className))))) 536 | 537 | (testing "Grabs enter class name from map argument" 538 | (let [el (js/document.createElement "div")] 539 | (sut/render (sut/CSSTransitionGroup {:transitionName {:enter "examplez-enter"}} []) el) 540 | (sut/render 541 | (sut/CSSTransitionGroup 542 | {:transitionName {:enter "examplez-enter"}} 543 | [(d/div {:key "#1"} "I will enter")]) 544 | el) 545 | (is (= "examplez-enter" (.. el -firstChild -firstChild -className))))) 546 | 547 | (testing "Does not add enter class name when enter transition is disabled" 548 | (let [el (js/document.createElement "div")] 549 | (sut/render (sut/CSSTransitionGroup {:transitionName "example" 550 | :transitionEnter false} []) el) 551 | (sut/render 552 | (sut/CSSTransitionGroup 553 | {:transitionName "example" 554 | :transitionEnter false} 555 | [(d/div {:key "#1"} "I will enter")]) 556 | el) 557 | (is (= "" (.. el -firstChild -firstChild -className))))) 558 | 559 | (testing "Adds enter class name to existing ones" 560 | (let [el (js/document.createElement "div")] 561 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 562 | (sut/render 563 | (sut/CSSTransitionGroup 564 | {:transitionName "example"} 565 | [(d/div {:key "#2" :className "item"} "I will enter")]) 566 | el) 567 | (is (= "item example-enter" (.. el -firstChild -firstChild -className))))) 568 | 569 | (testing "Adds enter-active class name on next tick" 570 | (async done 571 | (let [el (js/document.createElement "div")] 572 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 573 | (sut/render 574 | (sut/CSSTransitionGroup 575 | {:transitionName "example"} 576 | [(d/div {:key "#3"} "I will enter")]) 577 | el) 578 | (js/setTimeout 579 | (fn [] 580 | (is (= "example-enter example-enter-active" (.. el -firstChild -firstChild -className))) 581 | (done)) 582 | 100)))) 583 | 584 | (testing "Adds custom enter-active class name on next tick" 585 | (async done 586 | (let [el (js/document.createElement "div") 587 | props {:transitionName {:enter "swoosh" :enterActive "lol"}}] 588 | (sut/render (sut/CSSTransitionGroup props []) el) 589 | (sut/render (sut/CSSTransitionGroup props [(d/div {:key "#3"} "I will enter")]) 590 | el) 591 | (js/setTimeout 592 | (fn [] 593 | (is (= "swoosh lol" (.. el -firstChild -firstChild -className))) 594 | (done)) 595 | 0)))) 596 | 597 | (testing "Uses custom enter class name for missing enterActive class name" 598 | (async done 599 | (let [el (js/document.createElement "div") 600 | props {:transitionName {:enter "swoosh"}}] 601 | (sut/render (sut/CSSTransitionGroup props []) el) 602 | (sut/render (sut/CSSTransitionGroup props [(d/div {:key "#3"} "I will enter")]) 603 | el) 604 | (js/setTimeout 605 | (fn [] 606 | (is (= "swoosh swoosh-active" (.. el -firstChild -firstChild -className))) 607 | (done)) 608 | 0)))) 609 | 610 | (testing "Removes enter transition class names after timeout" 611 | (async done 612 | (let [el (js/document.createElement "div")] 613 | (sut/render (sut/CSSTransitionGroup {:transitionName "example" 614 | :transitionEnterTimeout 10} []) el) 615 | (sut/render 616 | (sut/CSSTransitionGroup 617 | {:transitionName "example" 618 | :transitionEnterTimeout 10} 619 | [(d/div {:key "#4" :className "do not remove"} "I will enter")]) 620 | el) 621 | (js/setTimeout 622 | (fn [] 623 | (is (= "do not remove" (.. el -firstChild -firstChild -className))) 624 | (done)) 625 | 10)))) 626 | 627 | (testing "Removes enter transition class names after completed transition" 628 | (async done 629 | (let [el (js/document.createElement "div") 630 | style (or (js/document.getElementById "transition-css") 631 | (let [style (js/document.createElement "style")] 632 | (set! (.-type style) "text/css") 633 | (set! (.-id style) "transition-css") 634 | (js/document.head.appendChild style) 635 | style))] 636 | (set! (.-innerHTML style) (str ".transition-example-enter {transition: color 10ms; color: #000;}" 637 | ".transition-example-enter-active {color: #f00;}")) 638 | (js/document.body.appendChild el) 639 | (sut/render (sut/CSSTransitionGroup {:transitionName "transition-example"} []) el) 640 | (sut/render 641 | (sut/CSSTransitionGroup 642 | {:transitionName "transition-example"} 643 | [(d/div {:key "#5"} "Check test document CSS")]) 644 | el) 645 | (js/setTimeout 646 | (fn [] 647 | (is (= "" (.. el -firstChild -firstChild -className))) 648 | (.removeChild (.-parentNode el) el) 649 | (done)) 650 | 100))))) 651 | 652 | (deftest CSSTransitionGroupAppearTest 653 | (testing "Adds appear class name according to the transition name" 654 | (let [el (js/document.createElement "div")] 655 | (sut/render 656 | (sut/CSSTransitionGroup 657 | {:transitionName "example" 658 | :transitionAppear true} 659 | [(d/div {:key "#1"} "I will appear")]) 660 | el) 661 | (is (= "example-appear" (.. el -firstChild -firstChild -className))))) 662 | 663 | (testing "Does not add appear class name when appear transition is disabled" 664 | (let [el (js/document.createElement "div")] 665 | (sut/render 666 | (sut/CSSTransitionGroup 667 | {:transitionName "example"} 668 | [(d/div {:key "#1"} "I will appear")]) 669 | el) 670 | (is (= "" (.. el -firstChild -firstChild -className))))) 671 | 672 | (testing "Adds appear class name to existing ones" 673 | (let [el (js/document.createElement "div")] 674 | (sut/render 675 | (sut/CSSTransitionGroup 676 | {:transitionName "example" 677 | :transitionAppear true} 678 | [(d/div {:key "#2" :className "item"} "I will appear")]) 679 | el) 680 | (is (= "item example-appear" (.. el -firstChild -firstChild -className))))) 681 | 682 | (testing "Adds appear-active class name on next tick" 683 | (async done 684 | (let [el (js/document.createElement "div")] 685 | (sut/render 686 | (sut/CSSTransitionGroup 687 | {:transitionName "example" 688 | :transitionAppear true} 689 | [(d/div {:key "#3"} "I will appear")]) 690 | el) 691 | (js/setTimeout 692 | (fn [] 693 | (is (= "example-appear example-appear-active" (.. el -firstChild -firstChild -className))) 694 | (done)) 695 | 0)))) 696 | 697 | (testing "Removes appear transition class names after timeout" 698 | (async done 699 | (let [el (js/document.createElement "div")] 700 | (sut/render 701 | (sut/CSSTransitionGroup 702 | {:transitionName "example" 703 | :transitionAppear true 704 | :transitionAppearTimeout 10} 705 | [(d/div {:key "#4" :className "do not remove"} "I will appear")]) 706 | el) 707 | (js/setTimeout 708 | (fn [] 709 | (is (= "do not remove" (.. el -firstChild -firstChild -className))) 710 | (done)) 711 | 10))))) 712 | 713 | (deftest CSSTransitionGroupLeaveTest 714 | (testing "Adds leave class name according to the transition name" 715 | (let [el (js/document.createElement "div")] 716 | (sut/render 717 | (sut/CSSTransitionGroup 718 | {:transitionName "example"} 719 | [(d/div {:key "#1"} "I will leave")]) 720 | el) 721 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 722 | (is (= "example-leave" (.. el -firstChild -firstChild -className))))) 723 | 724 | (testing "Adds leave class name to existing ones" 725 | (let [el (js/document.createElement "div")] 726 | (sut/render 727 | (sut/CSSTransitionGroup 728 | {:transitionName "example"} 729 | [(d/div {:key "#2" :className "item"} "I will leave")]) 730 | el) 731 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 732 | (is (= "item example-leave" (.. el -firstChild -firstChild -className))))) 733 | 734 | (testing "Adds leave-active class name on next tick" 735 | (async done 736 | (let [el (js/document.createElement "div")] 737 | (sut/render 738 | (sut/CSSTransitionGroup 739 | {:transitionName "example"} 740 | [(d/div {:key "#3"} "I will leave")]) 741 | el) 742 | (sut/render (sut/CSSTransitionGroup {:transitionName "example"} []) el) 743 | (js/setTimeout 744 | (fn [] 745 | (is (= "example-leave example-leave-active" (.. el -firstChild -firstChild -className))) 746 | (done)) 747 | 0)))) 748 | 749 | (testing "Removes node after leave timeout" 750 | (async done 751 | (let [el (js/document.createElement "div")] 752 | (sut/render 753 | (sut/CSSTransitionGroup 754 | {:transitionName "example" 755 | :transitionLeaveTimeout 10} 756 | [(d/div {:key "#4"} "I will leave")]) 757 | el) 758 | (sut/render (sut/CSSTransitionGroup {:transitionName "example" 759 | :transitionLeaveTimeout 10} []) el) 760 | (js/setTimeout 761 | (fn [] 762 | (is (nil? (.. el -firstChild -firstChild))) 763 | (done)) 764 | 10))))) 765 | 766 | (deftest EagerRenderModeTest 767 | (testing "Does not re-render old components when new ones are defined by default" 768 | (let [el (js/document.createElement "div") 769 | on-render (atom []) 770 | comp (sut/component 771 | (fn [_] (d/div {} "Testing")) 772 | {:on-render (fn [node data & args] 773 | (swap! on-render conj data))})] 774 | (sut/render (comp {:a 42}) el) 775 | (sut/component (fn [_] (d/div {} "Created another component"))) 776 | (sut/render (comp {:a 42}) el) 777 | (is (= 1 (count @on-render))))) 778 | 779 | (testing "Re-renders old components when new ones are defined when configured to" 780 | (with-redefs [dumdom.component/*render-eagerly?* true] 781 | (let [el (js/document.createElement "div") 782 | on-render (atom []) 783 | comp (sut/component 784 | (fn [_] (d/div {} "Testing")) 785 | {:on-render (fn [node data & args] 786 | (swap! on-render conj data))})] 787 | (sut/render (comp {:a 42}) el) 788 | (sut/component (fn [_] (d/div {} "Created another component"))) 789 | (sut/render (comp {:a 42}) el) 790 | (is (= 2 (count @on-render)))))) 791 | 792 | (testing "Only re-renders old components after new ones are defined" 793 | (with-redefs [dumdom.component/*render-eagerly?* true] 794 | (let [el (js/document.createElement "div") 795 | on-render (atom []) 796 | comp (sut/component 797 | (fn [_] (d/div {} "Testing")) 798 | {:on-render (fn [node data & args] 799 | (swap! on-render conj data))})] 800 | (sut/render (comp {:a 42}) el) 801 | (sut/component (fn [_] (d/div {} "Created another component"))) 802 | (sut/render (comp {:a 42}) el) 803 | (sut/render (comp {:a 42}) el) 804 | (is (= 2 (count @on-render))))))) 805 | --------------------------------------------------------------------------------