├── src ├── prum │ ├── compiler.cljs │ ├── react │ │ ├── cache.clj │ │ └── inline.clj │ ├── util.cljc │ ├── cljss │ │ └── compiler.clj │ ├── derived_atom.cljc │ ├── compiler.clj │ ├── cursor.cljs │ ├── cursor.clj │ ├── input.cljs │ ├── core.clj │ ├── core.cljs │ └── server_render.clj └── deps.cljs ├── examples └── prum │ ├── examples │ ├── transition.cljs │ ├── local_state.cljc │ ├── self_reference.cljc │ ├── cljss.cljs │ ├── keys.cljc │ ├── timer_static.cljc │ ├── timer_reactive.cljc │ ├── context.cljs │ ├── controls.cljc │ ├── board_reactive.cljc │ ├── custom_props.cljs │ ├── binary_clock.cljc │ ├── bmi_calculator.cljc │ ├── refs.cljc │ ├── core.cljc │ ├── form_validation.cljs │ ├── inputs.cljc │ └── swap_recognizer.cljs │ ├── examples.cljs │ └── examples_page.clj ├── CHANGELOG.md ├── package.json ├── .gitignore ├── test └── prum │ └── test │ ├── react_render_html.js │ ├── server.clj │ ├── derived_atom.clj │ ├── cursor.clj │ └── defc.clj ├── perf ├── pages │ └── page1.html └── prum │ └── perf.clj ├── project.clj ├── index.html ├── LICENSE └── README.md /src/prum/compiler.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.compiler) 2 | -------------------------------------------------------------------------------- /src/prum/react/cache.clj: -------------------------------------------------------------------------------- 1 | (ns prum.react.cache) 2 | -------------------------------------------------------------------------------- /examples/prum/examples/transition.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.transition) 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.8-4 2 | - Update Sablono with compatibility fix for `:on-change` events 3 | -------------------------------------------------------------------------------- /src/deps.cljs: -------------------------------------------------------------------------------- 1 | {:npm-deps {"prum-preact" "8.2.5" 2 | "preact-render-to-string" "3.6.3"}} 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@cljs-oss/module-deps": "^1.1.1", 4 | "preact-render-to-string": "^3.6.3", 5 | "prum-preact": "^8.2.5" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/** 2 | !/target/main.js 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .DS_Store 12 | .nrepl-history 13 | -------------------------------------------------------------------------------- /src/prum/util.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.util) 2 | 3 | 4 | (defn collect [key mixins] 5 | (->> (map (fn [m] (get m key)) mixins) 6 | (remove nil?))) 7 | 8 | 9 | (defn collect* [keys mixins] 10 | (->> (mapcat (fn [m] (map (fn [k] (get m k)) keys)) mixins) 11 | (remove nil?))) 12 | 13 | 14 | (defn call-all [state fns & args] 15 | (reduce 16 | (fn [state fn] 17 | (apply fn state args)) 18 | state 19 | fns)) 20 | -------------------------------------------------------------------------------- /test/prum/test/react_render_html.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var fs = require('fs'), 4 | vm = require('vm'); 5 | 6 | global.goog = {}; 7 | 8 | global.CLOSURE_IMPORT_SCRIPT = function(src) { 9 | require('./target/none/goog/' + src); 10 | return true; 11 | }; 12 | 13 | function nodeGlobalRequire(file) { 14 | vm.runInThisContext.call(global, fs.readFileSync(file), file); 15 | } 16 | 17 | nodeGlobalRequire('./target/test.js'); 18 | 19 | prum.test.server_render.react_render_html(fs.writeFileSync); 20 | -------------------------------------------------------------------------------- /examples/prum/examples/local_state.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.local-state 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;; Local component state 8 | 9 | 10 | (prum/defcs local-state < (prum/local 0) 11 | [state title] 12 | (let [*count (:prum/local state)] 13 | [:div 14 | {:style {"-webkit-user-select" "none" 15 | "cursor" "pointer"} 16 | :on-click (fn [_] (swap! *count inc))} 17 | title ": " @*count])) 18 | 19 | 20 | #?(:cljs 21 | (defn mount! [mount-el] 22 | (prum/mount (local-state "Clicks count") mount-el))) 23 | -------------------------------------------------------------------------------- /examples/prum/examples/self_reference.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.self-reference 2 | (:require 3 | [prum.core :as prum])) 4 | 5 | 6 | ;; Self-referencing component 7 | 8 | 9 | (prum/defc self-reference < prum/static 10 | ([form] (self-reference form 0)) 11 | ([form depth] 12 | (if (sequential? form) 13 | [:.branch {:style {:margin-left (* 10 depth)}} (map #(self-reference % (inc depth)) form)] 14 | [:.leaf {:style {:margin-left (* 10 depth)}} (str form)]))) 15 | 16 | 17 | #?(:cljs 18 | (defn mount! [mount-el] 19 | (prum/mount (self-reference [:a [:b [:c :d [:e] :g]]]) mount-el))) 20 | -------------------------------------------------------------------------------- /examples/prum/examples/cljss.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.cljss 2 | (:require [prum.core :as prum] 3 | [clojure.string :as cstr] 4 | [cljss.core])) 5 | 6 | (def c [:0 :1 :2 :3 :4 :5 :6 :7 :8 :9 :A :B :C :D :E :F]) 7 | 8 | (defn rand-color [] 9 | (str "#" (cstr/join (map name (repeatedly 6 #(rand-nth c)))))) 10 | 11 | 12 | (prum/defcs button < 13 | (prum/local "#fff" ::color) 14 | [{color ::color}] 15 | [:button {:on-click #(reset! color (rand-color)) 16 | :css {:background-color @color}} 17 | "Press to change color"]) 18 | 19 | (defn mount! [mount-el] 20 | (prum/mount (button) mount-el)) 21 | -------------------------------------------------------------------------------- /examples/prum/examples/keys.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.keys 2 | (:refer-clojure :exclude [keys]) 3 | (:require 4 | [prum.core :as prum] 5 | [prum.examples.core :as core])) 6 | 7 | 8 | (prum/defc keyed < {:key-fn (fn [label number] 9 | (str label "-" number))} 10 | [label number] 11 | [:div {} (str label "-" number)]) 12 | 13 | 14 | (prum/defc keys [] 15 | [:div {} 16 | (map 17 | (fn [[label num key]] 18 | (if key 19 | (prum/with-key (keyed "a" 1) key) 20 | (keyed label num))) 21 | [["a" 1] 22 | ["a" 2] 23 | ["b" 1] 24 | ["a" 1 "x"]])]) 25 | 26 | 27 | 28 | #?(:cljs 29 | (defn mount! [mount-el] 30 | (prum/mount (keys) mount-el))) 31 | -------------------------------------------------------------------------------- /src/prum/cljss/compiler.clj: -------------------------------------------------------------------------------- 1 | (ns prum.cljss.compiler 2 | (:require [cljss.builder :as builder])) 3 | 4 | (defn- compile-class [class styles] 5 | (let [cls (str "css-" (hash styles)) 6 | gen-class `(cljss.core/css ~@(builder/build-styles cls styles))] 7 | (if (seq class) 8 | `(str ~gen-class " " ~@(interpose " " class)) 9 | gen-class))) 10 | 11 | (defn compile-css-attr [attrs] 12 | (if-let [styles (:css attrs)] 13 | (let [{:keys [class className class-name]} attrs 14 | class (->> [class className class-name] 15 | (mapcat identity) 16 | (filter identity))] 17 | (-> attrs 18 | (dissoc :css) 19 | (assoc :class (compile-class class styles)))) 20 | attrs)) 21 | -------------------------------------------------------------------------------- /examples/prum/examples/timer_static.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.timer-static 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;; Static component (quiescent-style) 9 | 10 | 11 | (prum/defc timer-static < prum/static [label ts] 12 | [:div {} label ": " 13 | [:span {:style {:color @core/*color}} (core/format-time ts)]]) 14 | 15 | 16 | #?(:cljs 17 | (defn mount! [mount-el] 18 | (let [root (atom nil)] 19 | (reset! root (prum/mount (timer-static "Static" @core/*clock) mount-el)) 20 | ;; Setting up watch manually, 21 | ;; force top-down re-render via mount 22 | (add-watch core/*clock :timer-static 23 | (fn [_ _ _ new-val] 24 | (reset! root (prum/mount (timer-static "Static" new-val) mount-el @root))))))) 25 | -------------------------------------------------------------------------------- /examples/prum/examples/timer_reactive.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.timer-reactive 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;; Reactive components (reagent-style) 9 | 10 | 11 | ;; regular static top-down component with immutable args 12 | (prum/defc colored-clock < prum/static [time color] 13 | [:span {:style {:color color}} (core/format-time time)]) 14 | 15 | 16 | (prum/defc timer-reactive < prum/reactive [] 17 | [:div {} "Reactive: " 18 | ;; Subscribing to atom changes with prum/react 19 | ;; Then pass _dereferenced values_ to static component 20 | (colored-clock (prum/react core/*clock) (prum/react core/*color))]) 21 | 22 | 23 | ;; After initial mount, all changes will be re-rendered automatically 24 | #?(:cljs 25 | (defn mount! [mount-el] 26 | (prum/mount (timer-reactive) mount-el))) 27 | -------------------------------------------------------------------------------- /src/prum/derived_atom.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.derived-atom) 2 | 3 | 4 | (defn derived-atom 5 | ([refs key f] 6 | (derived-atom refs key f {})) 7 | ([refs key f opts] 8 | (let [{:keys [ref check-equals?] 9 | :or {check-equals? true}} opts 10 | recalc (case (count refs) 11 | 1 (let [[a] refs] #(f @a)) 12 | 2 (let [[a b] refs] #(f @a @b)) 13 | 3 (let [[a b c] refs] #(f @a @b @c)) 14 | #(apply f (map deref refs))) 15 | sink (if ref 16 | (doto ref (reset! (recalc))) 17 | (atom (recalc))) 18 | watch (if check-equals? 19 | (fn [_ _ _ _] 20 | (let [new-val (recalc)] 21 | (when (not= @sink new-val) 22 | (reset! sink new-val)))) 23 | (fn [_ _ _ _] 24 | (reset! sink (recalc))))] 25 | (doseq [ref refs] 26 | (add-watch ref key watch)) 27 | sink))) 28 | -------------------------------------------------------------------------------- /examples/prum/examples/context.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.context 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | ;; Components with context that all descendants have access to implicitly. 7 | 8 | ;; This is useful when you are using child components you cannot modify. 9 | ;; For example, a JS library that gives you components which rely on a context 10 | ;; value being set by an ancestor component. 11 | 12 | 13 | (prum/defcc prum-context-comp [comp] 14 | [:span 15 | {:style {:color (prum/context comp :color)}} 16 | "Child component uses context to set font color."]) 17 | 18 | 19 | ;; Assume the following component is from our source code. 20 | (def color-theme 21 | {:child-context (fn [state] {:color @core/*color})}) 22 | 23 | (prum/defc context < color-theme [] 24 | [:div {} 25 | [:div {} "Root component implicitly passes data to descendants."] 26 | (prum-context-comp)]) 27 | 28 | 29 | (defn mount! [mount-el] 30 | (prum/mount (context) mount-el)) 31 | -------------------------------------------------------------------------------- /src/prum/react/inline.clj: -------------------------------------------------------------------------------- 1 | (ns prum.react.inline 2 | (:require [clojure.spec.alpha :as s] 3 | [hicada.compiler :as h])) 4 | 5 | (s/def ::element 6 | (s/and 7 | (s/conformer #(if (keyword? %) (name %) %)))) 8 | 9 | (s/def ::static-element 10 | (s/cat 11 | :tag ::element 12 | :props map? 13 | :children (s/? (s/spec some?)))) 14 | 15 | (defn parse-static-element [element] 16 | (s/conform ::static-element element)) 17 | 18 | (defn inline-element [[tag props children]] 19 | (let [result (parse-static-element [tag props children])] 20 | (if (not= ::s/invalid result) 21 | (let [{:keys [tag props children]} result 22 | key (or (:key props) 'js/undefined) 23 | attrs (-> props (dissoc :ref :key) h/compile-config h/to-js) 24 | children '(cljs.core/array children)] 25 | (h/to-js 26 | {:nodeName tag 27 | :key key 28 | :attrs attrs 29 | :children children})) 30 | (throw (Error. (str "Element " tag " can not be inlined")))))) 31 | -------------------------------------------------------------------------------- /test/prum/test/server.clj: -------------------------------------------------------------------------------- 1 | (ns prum.test.server 2 | (:require 3 | [prum.core :as prum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | 7 | (prum/defcs comp-mixins < (prum/local 7) 8 | {:will-mount (fn [s] (assoc s ::key 1))} 9 | [state] 10 | [:div 11 | [:.local @(:prum/local state)] 12 | [:.key (::key state)]]) 13 | 14 | 15 | (deftest test-lifecycle 16 | (is (= (comp-mixins) 17 | [:div 18 | [:.local 7] 19 | [:.key 1]]))) 20 | 21 | 22 | (prum/defc comp-arglists 23 | ([a]) 24 | ([a b]) 25 | ([a b c])) 26 | 27 | 28 | (prum/defcc comp-arglists-1 29 | ([comp a]) 30 | ([comp a b]) 31 | ([comp a b c])) 32 | 33 | 34 | (deftest test-arglists 35 | (is (= (:arglists (meta #'comp-mixins)) 36 | '([]))) 37 | (is (= (:arglists (meta #'comp-arglists)) 38 | '([a] [a b] [a b c]))) 39 | (is (= (:arglists (meta #'comp-arglists-1)) 40 | '([a] [a b] [a b c])))) 41 | -------------------------------------------------------------------------------- /examples/prum/examples/controls.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.controls 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;; Control panel 9 | 10 | 11 | ;; generic “atom editor” component 12 | (prum/defc input < prum/reactive [ref] 13 | [:input {:type "text" 14 | :value (prum/react ref) 15 | :style {:width 100} 16 | :on-change #(reset! ref (.. % -target -value))}]) 17 | 18 | 19 | ;; Raw top-level component, everything interesting is happening inside 20 | (prum/defc controls [] 21 | [:dl {} 22 | [:dt {} "Color: "] 23 | [:dd {} (input core/*color)] 24 | ;; Binding another component to the same atom will keep 2 input boxes in sync 25 | [:dt {} "Clone: "] 26 | [:dd {} (input core/*color)] 27 | [:dt {} "Color: "] 28 | [:dd {} (core/watches-count core/*color) " watches"] 29 | 30 | [:dt {} "Tick: "] 31 | [:dd {} (input core/*speed) " ms"] 32 | [:dt {} "Time:"] 33 | [:dd {} (core/watches-count core/*clock) " watches"]]) 34 | 35 | 36 | 37 | #?(:cljs 38 | (defn mount! [mount-el] 39 | (prum/mount (controls) mount-el))) 40 | 41 | -------------------------------------------------------------------------------- /src/prum/compiler.clj: -------------------------------------------------------------------------------- 1 | (ns prum.compiler 2 | (:require [hicada.compiler :as h] 3 | [prum.react.inline :as inline] 4 | [prum.cljss.compiler :as css])) 5 | 6 | (defn normalize-attrs [tag {:keys [type on-change onChange] :as attrs}] 7 | (let [on-change (or on-change onChange)] 8 | (if (and on-change 9 | (or (= tag :textarea) 10 | (and (= tag :input) 11 | (or (nil? type) 12 | (->> type (re-matches #"^(fil|che|rad).*") nil?))))) 13 | (-> attrs 14 | (dissoc :on-change :onChange) 15 | (assoc :on-input on-change)) 16 | attrs))) 17 | 18 | (defn transform-fn [[tag attrs children {:keys [inline? css-attr?]}]] 19 | (let [attrs (if css-attr? (css/compile-css-attr attrs) attrs) 20 | ret [tag (normalize-attrs tag attrs) children]] 21 | (if inline? 22 | (inline/inline-element ret) 23 | ret))) 24 | 25 | (defn compile-html [hiccup] 26 | (h/compile 27 | hiccup 28 | {:create-element 'prum-preact/createElement 29 | :transform-fn transform-fn} 30 | nil 31 | {:css-attr? true})) 32 | 33 | (defmacro html [hiccup] 34 | (compile-html hiccup)) 35 | -------------------------------------------------------------------------------- /examples/prum/examples/board_reactive.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.board-reactive 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;; Reactive drawing board 8 | 9 | 10 | (def *board (atom (core/initial-board))) 11 | (def *board-renders (atom 0)) 12 | 13 | 14 | (prum/defc cell < prum/reactive [x y] 15 | (swap! *board-renders inc) 16 | (let [*cursor (prum/cursor-in *board [y x])] 17 | ;; each cell subscribes to its own cursor inside a board 18 | ;; note that subscription to color is conditional: 19 | ;; only if cell is on (@cursor == true), 20 | ;; this component will be notified on color changes 21 | [:div.art-cell {:style {:background-color (when (prum/react *cursor) (prum/react core/*color))} 22 | :on-mouse-over (fn [_] (swap! *cursor not) nil)}])) 23 | 24 | 25 | (prum/defc board-reactive [] 26 | [:div.artboard {} 27 | (for [y (range 0 core/board-height)] 28 | [:div.art-row {:key y} 29 | (for [x (range 0 core/board-width)] 30 | ;; this is how one can specify React key for component 31 | (-> (cell x y) 32 | (prum/with-key [x y])))]) 33 | (core/board-stats *board *board-renders)]) 34 | 35 | 36 | #?(:cljs 37 | (defn mount! [mount-el] 38 | (prum/mount (board-reactive) mount-el))) 39 | -------------------------------------------------------------------------------- /examples/prum/examples/custom_props.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.custom-props 2 | (:require 3 | [prum.core :as prum] 4 | [prum.compiler :refer-macros [html]] 5 | [goog.object :as gobj] 6 | [prum.examples.core :as core])) 7 | 8 | 9 | ;; Custom methods and data on the underlying React components. 10 | 11 | (defn rand-color [] 12 | (str "#" (-> (rand) 13 | (* 0xffffff) 14 | (js/Math.floor) 15 | (.toString 16)))) 16 | 17 | (def props 18 | {:msgData "Components can store custom data on the underlying React component." 19 | :msgMethod #(this-as this 20 | (html 21 | [:div {:style {:cursor "pointer"} 22 | :on-mouse-move 23 | (fn [_] 24 | (reset! core/*color (rand-color)) 25 | (gobj/set this "msgData" 26 | (html 27 | [:div {:style {:color @core/*color}} 28 | (:msgData props)])) 29 | (prum/request-render this))} 30 | "Custom methods too. Hover me!"]))}) 31 | 32 | 33 | (prum/defcc custom-props < {:class-properties props} [this] 34 | [:div {} 35 | [:div {} (gobj/get this "msgData")] 36 | [:div {} (.call (gobj/get this "msgMethod") this)]]) 37 | 38 | (defn mount! [mount-el] 39 | (prum/mount (custom-props) mount-el)) 40 | -------------------------------------------------------------------------------- /src/prum/cursor.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.cursor) 2 | 3 | 4 | (deftype Cursor [ref path meta] 5 | Object 6 | (equiv [this other] 7 | (-equiv this other)) 8 | 9 | IAtom 10 | 11 | IMeta 12 | (-meta [_] meta) 13 | 14 | IEquiv 15 | (-equiv [this other] 16 | (identical? this other)) 17 | 18 | IDeref 19 | (-deref [_] 20 | (get-in (-deref ref) path)) 21 | 22 | IWatchable 23 | (-add-watch [this key callback] 24 | (add-watch ref (list this key) 25 | (fn [_ _ oldv newv] 26 | (let [old (get-in oldv path) 27 | new (get-in newv path)] 28 | (when (not= old new) 29 | (callback key this old new))))) 30 | this) 31 | 32 | (-remove-watch [this key] 33 | (remove-watch ref (list this key)) 34 | this) 35 | 36 | IHash 37 | (-hash [this] (goog/getUid this)) 38 | 39 | IReset 40 | (-reset! [_ newv] 41 | (swap! ref assoc-in path newv) 42 | newv) 43 | 44 | ISwap 45 | (-swap! [this f] 46 | (-reset! this (f (-deref this)))) 47 | (-swap! [this f a] 48 | (-reset! this (f (-deref this) a))) 49 | (-swap! [this f a b] 50 | (-reset! this (f (-deref this) a b))) 51 | (-swap! [this f a b rest] 52 | (-reset! this (apply f (-deref this) a b rest))) 53 | 54 | IPrintWithWriter 55 | (-pr-writer [this writer opts] 56 | (-write writer "#object [prum.cursor.Cursor ") 57 | (pr-writer {:val (-deref this)} writer opts) 58 | (-write writer "]"))) 59 | 60 | -------------------------------------------------------------------------------- /perf/pages/page1.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 10 |
11 |

2016

12 |

The Blessing of Interactive Development 4/11 13 |

2015

14 |

The Web After Tomorrow 6/23 15 |

A shallow dive into DataScript internals 2/23 16 |

2014

17 |

Couple of DataScript resources 12/18 18 |

Streams: Mail 3.0 concept 10/27 19 |

Another powered-by-DataScript example 10/6 20 |

Chatting cats use DataScript for fun 9/18 21 |

Irrelevant Things 8/12 22 |

Reinventing Git interface 6/17 23 |

Unofficial guide to Datomic internals 5/6 24 |

Datomic as a Protocol 4/29 25 |

Decomposing web app development 4/24 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/prum/test/derived_atom.clj: -------------------------------------------------------------------------------- 1 | (ns prum.test.derived-atom 2 | (:require 3 | [prum.core :as prum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | 7 | (deftest test-derived 8 | (let [*a (atom 0) 9 | *b (atom "x") 10 | *d (prum/derived-atom [*a *b] ::key 11 | (fn [a b] 12 | (str a ":" b)))] 13 | (is (= "0:x" @*d)) 14 | (swap! *a inc) 15 | (is (= "1:x" @*d)) 16 | (reset! *b "y") 17 | (is (= "1:y" @*d))) 18 | 19 | (testing "user-provided ref" 20 | (let [*a (atom 0) 21 | *d (atom nil)] 22 | (prum/derived-atom [*a] ::key str {:ref *d}) 23 | (is (= "0" @*d)) 24 | (swap! *a inc) 25 | (is (= "1" @*d)))) 26 | 27 | (testing "check-equals" 28 | (let [*a (atom 0) 29 | *d (atom nil) 30 | *resets (atom 0)] 31 | (add-watch *d ::count-resets (fn [_ _ _ _] (swap! *resets inc))) 32 | (prum/derived-atom [*a] ::key #(mod % 10) {:ref *d}) 33 | (is (= 0 @*d)) 34 | (is (= 1 @*resets)) 35 | (reset! *a 1) 36 | (is (= 1 @*d)) 37 | (is (= 2 @*resets)) 38 | (reset! *a 11) ;; *a changes, 39 | (is (= 1 @*d)) ;; but *d does not 40 | (is (= 2 @*resets))) ;; should not register reset! 41 | 42 | (let [*a (atom 0) 43 | *d (atom nil) 44 | *resets (atom 0)] 45 | (add-watch *d ::count-resets (fn [_ _ _ _] (swap! *resets inc))) 46 | (prum/derived-atom [*a] ::key #(mod % 10) {:ref *d :check-equals? false}) 47 | (is (= 0 @*d)) 48 | (is (= 1 @*resets)) 49 | (reset! *a 1) 50 | (is (= 1 @*d)) 51 | (is (= 2 @*resets)) 52 | (reset! *a 11) 53 | (is (= 1 @*d)) 54 | (is (= 3 @*resets))))) ;; should register reset! anyways 55 | 56 | -------------------------------------------------------------------------------- /examples/prum/examples/binary_clock.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.binary-clock 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;; Binary clock 8 | 9 | 10 | (def *bclock-renders (atom 0)) 11 | 12 | 13 | (prum/defc render-count < prum/reactive [ref] 14 | [:div.stats {} "Renders: " (prum/react ref)]) 15 | 16 | 17 | (prum/defc bit < prum/static [n bit] 18 | (let [color (when (bit-test n bit) @core/*color)] 19 | (swap! *bclock-renders inc) 20 | [:td.bclock-bit {:style {:backgroundColor color}}])) 21 | 22 | 23 | (prum/defc binary-clock < prum/reactive [] 24 | (let [ts (prum/react core/*clock) 25 | msec (mod ts 1000) 26 | sec (mod (quot ts 1000) 60) 27 | min (mod (quot ts 60000) 60) 28 | hour (mod (quot ts 3600000) 24) 29 | hh (quot hour 10) 30 | hl (mod hour 10) 31 | mh (quot min 10) 32 | ml (mod min 10) 33 | sh (quot sec 10) 34 | sl (mod sec 10) 35 | msh (quot msec 100) 36 | msm (-> msec (quot 10) (mod 10)) 37 | msl (mod msec 10)] 38 | [:table.bclock {} 39 | [:tbody {} 40 | [:tr {} [:td] (bit hl 3) [:th] [:td] (bit ml 3) [:th] [:td] (bit sl 3) [:th] (bit msh 3) (bit msm 3) (bit msl 3)] 41 | [:tr {} [:td] (bit hl 2) [:th] (bit mh 2) (bit ml 2) [:th] (bit sh 2) (bit sl 2) [:th] (bit msh 2) (bit msm 2) (bit msl 2)] 42 | [:tr {} (bit hh 1) (bit hl 1) [:th] (bit mh 1) (bit ml 1) [:th] (bit sh 1) (bit sl 1) [:th] (bit msh 1) (bit msm 1) (bit msl 1)] 43 | [:tr {} (bit hh 0) (bit hl 0) [:th] (bit mh 0) (bit ml 0) [:th] (bit sh 0) (bit sl 0) [:th] (bit msh 0) (bit msm 0) (bit msl 0)] 44 | [:tr {} [:th {} hh] [:th {} hl] [:th] [:th {} mh] [:th {} ml] [:th] [:th {} sh] [:th {} sl] [:th] [:th {} msh] [:th {} msm] [:th {} msl]] 45 | [:tr {} [:th {:col-span 8} (render-count *bclock-renders)]]]])) 46 | 47 | 48 | #?(:cljs 49 | (defn mount! [mount-el] 50 | (prum/mount (binary-clock) mount-el))) 51 | -------------------------------------------------------------------------------- /examples/prum/examples/bmi_calculator.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.bmi-calculator 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | 7 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 8 | ;; Reagent stype BMI calculator 9 | 10 | 11 | (def *bmi-data (atom {:height 180 12 | :weight 80})) 13 | 14 | 15 | (defn calc-bmi [{:keys [height weight bmi] :as data}] 16 | (let [h (/ height 100)] 17 | (if (nil? bmi) 18 | (assoc data :bmi (/ weight (* h h))) 19 | (assoc data :weight (* bmi h h))))) 20 | 21 | 22 | (prum/defc slider [param value min max] 23 | (let [reset (case param 24 | :bmi :weight 25 | :bmi)] 26 | [:input {:type "range" 27 | :value (int value) 28 | :min min 29 | :max max 30 | :style {:width "100%"} 31 | :on-change #(swap! *bmi-data assoc 32 | param (-> % .-target .-value) 33 | reset nil)}])) 34 | 35 | 36 | (prum/defc bmi-calculator < prum/reactive [] 37 | (let [{:keys [weight height bmi] :as data} (calc-bmi (prum/react *bmi-data)) 38 | [color diagnose] (cond 39 | (< bmi 18.5) ["orange" "underweight"] 40 | (< bmi 25) ["inherit" "normal"] 41 | (< bmi 30) ["orange" "overweight"] 42 | :else ["red" "obese"])] 43 | [:div.bmi {} 44 | [:div {} 45 | "Height: " (int height) "cm" 46 | (slider :height height 100 220)] 47 | [:div {} 48 | "Weight: " (int weight) "kg" 49 | (slider :weight weight 30 150)] 50 | [:div {} 51 | "BMI: " (int bmi) " " 52 | [:span {:style {:color color}} diagnose] 53 | (slider :bmi bmi 10 50)]])) 54 | 55 | 56 | ;; After initial mount, all changes will be re-rendered automatically 57 | #?(:cljs 58 | (defn mount! [mount-el] 59 | (prum/mount (bmi-calculator) mount-el))) 60 | -------------------------------------------------------------------------------- /examples/prum/examples/refs.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.refs 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core])) 5 | 6 | (prum/defc textarea [{:keys [on-change]}] 7 | [:textarea 8 | {:style {:width "100%" 9 | :padding "10px" 10 | :font "inherit" 11 | :outline "none" 12 | :resize "none"} 13 | :default-value "Auto-resizing\ntextarea" 14 | :placeholder "Auto-resizing textarea" 15 | :on-change on-change}]) 16 | 17 | (prum/defcc tac < {:after-render 18 | (fn [state] 19 | #?(:cljs 20 | (let [tac (prum/ref-node state :tac)] 21 | (set! (.-height (.-style tac)) "0") 22 | (set! (.-height (.-style tac)) (str (+ 2 (.-scrollHeight tac)) "px")))) 23 | state)} 24 | [comp] 25 | (prum/with-ref 26 | (textarea {:on-change #(prum/request-render comp)}) 27 | (prum/use-ref comp :tac))) 28 | 29 | 30 | (prum/defcc ta < {:after-render 31 | (fn [state] 32 | #?(:cljs 33 | (let [ta (prum/ref state :ta)] 34 | (set! (.-height (.-style ta)) "0") 35 | (set! (.-height (.-style ta)) (str (+ 2 (.-scrollHeight ta)) "px")))) 36 | state)} 37 | [comp] 38 | [:textarea 39 | {:ref (prum/use-ref comp :ta) 40 | :style {:width "100%" 41 | :padding "10px" 42 | :font "inherit" 43 | :outline "none" 44 | :resize "none"} 45 | :default-value "Auto-resizing\ntextarea" 46 | :placeholder "Auto-resizing textarea" 47 | :on-change (fn [_] (prum/request-render comp))}]) 48 | 49 | 50 | (prum/defc refs [] 51 | [:div {} 52 | (ta) 53 | (tac)]) 54 | 55 | 56 | #?(:cljs 57 | (defn mount! [mount-el] 58 | (prum/mount (refs) mount-el))) 59 | -------------------------------------------------------------------------------- /examples/prum/examples/core.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.core 2 | (:require 3 | [prum.core :as prum])) 4 | 5 | 6 | (def *clock (atom 0)) 7 | (def *color (atom "#FA8D97")) 8 | (def *speed (atom 167)) 9 | 10 | 11 | #?(:clj 12 | (def formatter 13 | (doto (java.text.SimpleDateFormat. "HH:mm:ss.SSS") 14 | (.setTimeZone (java.util.TimeZone/getTimeZone "UTC"))))) 15 | 16 | 17 | (defn format-time [ts] 18 | #?(:cljs (-> ts (js/Date.) (.toISOString) (subs 11 23)) 19 | :clj (.format formatter (java.util.Date. ts)))) 20 | 21 | 22 | #?(:cljs 23 | (defn el [id] 24 | (js/document.getElementById id))) 25 | 26 | 27 | ;; Custom mixin for updating components on timer 28 | ;; for cases where you have nothing to subscribe to 29 | (defn periodic-refresh [period] 30 | #?(:cljs 31 | {:did-mount 32 | (fn [state] 33 | (let [react-comp (:prum/react-component state) 34 | interval (js/setInterval #(prum/request-render react-comp) period)] 35 | (assoc state ::interval interval))) 36 | :will-unmount 37 | (fn [state] 38 | (js/clearInterval (::interval state)))} 39 | :clj {})) 40 | 41 | 42 | ;; Using custom mixin 43 | (prum/defc watches-count < (periodic-refresh 1000) [ref] 44 | [:span {} (count #?(:cljs (.-watches ref) 45 | :clj (.getWatches ^clojure.lang.IRef ref)))]) 46 | 47 | 48 | ;; Generic board utils 49 | 50 | 51 | (def ^:const board-width 19) 52 | (def ^:const board-height 10) 53 | 54 | 55 | (defn prime? [i] 56 | (and (>= i 2) 57 | (empty? (filter #(= 0 (mod i %)) (range 2 i))))) 58 | 59 | 60 | (defn initial-board [] 61 | (->> (map prime? (range 0 (* board-width board-height))) 62 | (partition board-width) 63 | (mapv vec))) 64 | 65 | 66 | (prum/defc board-stats < prum/reactive [*board *renders] 67 | [:div.stats {} 68 | "Renders: " (prum/react *renders) 69 | [:br] 70 | "Board watches: " (watches-count *board) 71 | [:br] 72 | "Color watches: " (watches-count *color)]) 73 | -------------------------------------------------------------------------------- /examples/prum/examples.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples 2 | (:require 3 | [clojure.string :as str] 4 | [prum.core :as prum] 5 | [prum.examples.core :as core] 6 | 7 | [prum.examples.timer-static :as timer-static] 8 | [prum.examples.timer-reactive :as timer-reactive] 9 | [prum.examples.controls :as controls] 10 | [prum.examples.binary-clock :as binary-clock] 11 | [prum.examples.board-reactive :as board-reactive] 12 | [prum.examples.bmi-calculator :as bmi-calculator] 13 | [prum.examples.form-validation :as form-validation] 14 | [prum.examples.inputs :as inputs] 15 | [prum.examples.refs :as refs] 16 | [prum.examples.local-state :as local-state] 17 | [prum.examples.keys :as keys] 18 | [prum.examples.self-reference :as self-reference] 19 | [prum.examples.context :as context] 20 | [prum.examples.custom-props :as custom-props] 21 | [prum.examples.swap-recognizer :as swap-recognizer] 22 | [prum.examples.cljss :as cljss])) 23 | 24 | 25 | (enable-console-print!) 26 | 27 | 28 | ;; Mount everything 29 | 30 | (timer-static/mount! (core/el "timer-static")) 31 | (timer-reactive/mount! (core/el "timer-reactive")) 32 | (controls/mount! (core/el "controls")) 33 | (binary-clock/mount! (core/el "binary-clock")) 34 | (board-reactive/mount! (core/el "board-reactive")) 35 | (bmi-calculator/mount! (core/el "bmi-calculator")) 36 | (form-validation/mount! (core/el "form-validation")) 37 | (inputs/mount! (core/el "inputs")) 38 | (refs/mount! (core/el "refs")) 39 | (local-state/mount! (core/el "local-state")) 40 | (keys/mount! (core/el "keys")) 41 | (self-reference/mount! (core/el "self-reference")) 42 | (context/mount! (core/el "context")) 43 | (custom-props/mount! (core/el "custom-props")) 44 | (swap-recognizer/mount! (core/el "swap-recognizer")) 45 | (cljss/mount! (core/el "cljss")) 46 | 47 | 48 | ;; Start clock ticking 49 | 50 | (defn tick [] 51 | (reset! core/*clock (.getTime (js/Date.))) 52 | (js/setTimeout tick @core/*speed)) 53 | 54 | 55 | (tick) 56 | -------------------------------------------------------------------------------- /src/prum/cursor.clj: -------------------------------------------------------------------------------- 1 | (ns prum.cursor) 2 | 3 | 4 | (deftype Cursor [ref path ^:volatile-mutable meta watches] 5 | clojure.lang.IDeref 6 | 7 | (deref [this] 8 | (get-in (deref ref) path)) 9 | 10 | clojure.lang.IRef 11 | 12 | (setValidator [this vf] 13 | (throw (UnsupportedOperationException. "prum.cursor.Cursor/setValidator"))) 14 | 15 | (getValidator [this] 16 | (throw (UnsupportedOperationException. "prum.cursor.Cursor/getValidator"))) 17 | 18 | (getWatches [this] 19 | @watches) 20 | 21 | (addWatch [this key callback] 22 | (vswap! watches assoc key callback) 23 | (add-watch ref (list this key) 24 | (fn [_ _ oldv newv] 25 | (let [old (get-in oldv path) 26 | new (get-in newv path)] 27 | (when (not= old new) 28 | (callback key this old new))))) 29 | this) 30 | 31 | (removeWatch [this key] 32 | (vswap! watches dissoc key) 33 | (remove-watch ref (list this key)) 34 | this) 35 | 36 | clojure.lang.IAtom 37 | 38 | (swap [this f] 39 | (-> (swap! ref update-in path f) 40 | (get-in path))) 41 | 42 | (swap [this f a] 43 | (-> (swap! ref update-in path f a) 44 | (get-in path))) 45 | 46 | (swap [this f a b] 47 | (-> (swap! ref update-in path f a b) 48 | (get-in path))) 49 | 50 | (swap [this f a b rest] 51 | (-> (apply swap! ref update-in path f a b rest) 52 | (get-in path))) 53 | 54 | (compareAndSet [this oldv newv] 55 | (loop [] 56 | (let [refv @ref] 57 | (if (not= oldv (get-in refv path)) 58 | false 59 | (or (compare-and-set! ref refv (assoc-in refv path newv)) 60 | (recur)))))) 61 | 62 | (reset [this newv] 63 | (swap! ref assoc-in path newv) 64 | newv) 65 | 66 | clojure.lang.IMeta 67 | 68 | (meta [this] 69 | meta) 70 | 71 | clojure.lang.IReference 72 | 73 | (alterMeta [this f args] 74 | (.resetMeta this (apply f meta args))) 75 | 76 | (resetMeta [this m] 77 | (set! meta m) 78 | m)) 79 | -------------------------------------------------------------------------------- /examples/prum/examples/form_validation.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.form-validation 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core] 5 | ["prum-preact" :as p])) 6 | 7 | 8 | (prum/defc validating-input < prum/reactive [ref f] 9 | [:input {:type "text" 10 | :style {:width 170 11 | :background-color (when-not (f (prum/react ref)) 12 | (prum/react core/*color))} 13 | :value (prum/react ref) 14 | :on-change #(reset! ref (.. % -target -value))}]) 15 | 16 | 17 | (prum/defcc restricting-input < prum/reactive [comp ref f] 18 | [:input {:type "text" 19 | :style {:width 170} 20 | :value (prum/react ref) 21 | :on-change #(let [new-val (.. % -target -value)] 22 | (if (f new-val) 23 | (reset! ref new-val) 24 | (prum/request-render comp)))}]) 25 | 26 | 27 | (prum/defcc restricting-input-native < prum/reactive [comp ref f] 28 | (p/createElement "input" 29 | #js {:type "text" 30 | :style #js {:width 170} 31 | :value (prum/react ref) 32 | :onInput #(let [new-val (.. % -target -value)] 33 | (if (f new-val) 34 | (reset! ref new-val) 35 | (prum/request-render comp)))})) 36 | 37 | 38 | (prum/defc form-validation [] 39 | (let [state (atom {:email "a@b.c" 40 | :phone "+7913 000 0000" 41 | :age "22"})] 42 | [:dl {} 43 | [:dt {} "E-mail:"] 44 | [:dd {} (validating-input (prum/cursor state :email) #(re-matches #"[^@]+@[^@.]+\..+" %))] 45 | [:dt {} "Phone:"] 46 | [:dd {} (restricting-input (prum/cursor state :phone) #(re-matches #"[0-9\- +()]*" %))] 47 | [:dt {} "Age:"] 48 | [:dd {} (restricting-input-native (prum/cursor state :age) #(re-matches #"([1-9][0-9]*)?" %))]])) 49 | 50 | 51 | (defn mount! [mount-el] 52 | (prum/mount (form-validation) mount-el)) 53 | -------------------------------------------------------------------------------- /examples/prum/examples/inputs.cljc: -------------------------------------------------------------------------------- 1 | (ns prum.examples.inputs 2 | (:require 3 | [clojure.string :as str] 4 | [prum.core :as prum])) 5 | 6 | 7 | (def values (range 1 5)) 8 | 9 | 10 | (prum/defc reactive-input < prum/reactive 11 | [*ref] 12 | (let [value (prum/react *ref)] 13 | [:input {:type "text" 14 | :value value 15 | :style {:width 170} 16 | :on-change (fn [e] (reset! *ref (long (.. e -currentTarget -value))))}])) 17 | 18 | 19 | (prum/defc checkboxes < prum/reactive 20 | [*ref] 21 | (let [value (prum/react *ref)] 22 | [:div {} 23 | (for [v values] 24 | [:input {:type "checkbox" 25 | :checked (= v value) 26 | :value v 27 | :on-click (fn [_] (reset! *ref v))}])])) 28 | 29 | 30 | (prum/defc radio < prum/reactive 31 | [*ref] 32 | (let [value (prum/react *ref)] 33 | [:div {} 34 | (for [v values] 35 | [:input {:type "radio" 36 | :name "inputs_radio" 37 | :checked (= v value) 38 | :value v 39 | :on-click (fn [_] (reset! *ref v))}])])) 40 | 41 | 42 | (prum/defc select < prum/reactive 43 | [*ref] 44 | (let [value (prum/react *ref)] 45 | [:select 46 | {:on-change (fn [e] (reset! *ref (long (.. e -target -value)))) 47 | :value value} 48 | (for [v values] 49 | [:option {:value v} v])])) 50 | 51 | 52 | (defn next-value [v] 53 | (loop [v' v] 54 | (if (= v v') 55 | (recur (rand-nth values)) 56 | v'))) 57 | 58 | 59 | (prum/defc shuffle-button < prum/reactive 60 | [*ref] 61 | [:button 62 | {:on-click (fn [_] 63 | (swap! *ref next-value))} 64 | "Next value"]) 65 | 66 | 67 | (prum/defc value < prum/reactive 68 | [*ref] 69 | [:code {} (pr-str (prum/react *ref))]) 70 | 71 | 72 | (prum/defc inputs [] 73 | (let [*ref (atom nil)] 74 | [:dl {} 75 | [:dt {} "Input"] [:dd {} (reactive-input *ref)] 76 | [:dt {} "Checks"] [:dd {} (checkboxes *ref)] 77 | [:dt {} "Radio"] [:dd {} (radio *ref)] 78 | [:dt {} "Select"] [:dd {} (select *ref)] 79 | [:dt {} (value *ref)] [:dd {} (shuffle-button *ref)]])) 80 | 81 | 82 | #?(:cljs 83 | (defn mount! [mount-el] 84 | (prum/mount (inputs) mount-el))) 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/prum/input.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.input 2 | (:require [clojure.string :refer [blank? join]] 3 | [goog.object :as object] 4 | [goog.functions :as gf] 5 | ["prum-preact" :as p])) 6 | 7 | (defn update-state 8 | "Updates the state of the wrapped input element." 9 | [component next-props property value] 10 | (let [on-change (object/getValueByKeys component "state" "onChange") 11 | next-state #js{}] 12 | (object/extend next-state next-props #js {:onChange on-change}) 13 | (object/set next-state property value) 14 | (.setState component next-state))) 15 | 16 | (defn wrap-form-element [element property] 17 | (let [ctor (fn [props] 18 | (this-as this 19 | (set! (.-state this) 20 | (let [state #js {}] 21 | (->> #js{:onChange (goog/bind (object/get this "onChange") this)} 22 | (object/extend state props)) 23 | state)) 24 | (.call p/Component this props)))] 25 | (set! (.-displayName ctor) (str "wrapped-" element)) 26 | (goog/inherits ctor p/Component) 27 | (specify! (.-prototype ctor) 28 | Object 29 | (onChange [this event] 30 | (when-let [handler (.-onChange (.-props this))] 31 | (handler event) 32 | (update-state 33 | this (.-props this) property 34 | (object/getValueByKeys event "target" property)))) 35 | 36 | (componentWillReceiveProps [this new-props] 37 | (let [state-value (object/getValueByKeys this "state" property) 38 | element-value (object/get (.-base this) property)] 39 | (if (not= state-value element-value) 40 | (update-state this new-props property element-value) 41 | (update-state this new-props property (object/get new-props property))))) 42 | 43 | (render [this] 44 | (p/createElement element (.-state this)))) 45 | ctor)) 46 | 47 | (def wrapped-input 48 | (gf/cacheReturnValue #(wrap-form-element "input" "value"))) 49 | 50 | (def wrapped-checked 51 | (gf/cacheReturnValue #(wrap-form-element "input" "checked"))) 52 | 53 | (def wrapped-select 54 | (gf/cacheReturnValue #(wrap-form-element "select" "value"))) 55 | 56 | (def wrapped-textarea 57 | (gf/cacheReturnValue #(wrap-form-element "textarea" "value"))) 58 | -------------------------------------------------------------------------------- /test/prum/test/cursor.clj: -------------------------------------------------------------------------------- 1 | (ns prum.test.cursor 2 | (:require 3 | [prum.core :as prum] 4 | [clojure.test :refer [deftest is are testing]])) 5 | 6 | 7 | (deftest test-cursor 8 | (let [a (atom {:b 1 :c {:cd 2}}) 9 | b (prum/cursor a :b) 10 | c (prum/cursor a :c) 11 | cd (prum/cursor c :cd) 12 | ccd (prum/cursor-in a [:c :cd])] 13 | (testing "deref" 14 | (is (= 1 @b)) 15 | (is (= {:cd 2} @c)) 16 | (is (= 2 @cd)) 17 | (is (= 2 @ccd))) 18 | 19 | (testing "swap!" 20 | (is (= 2 (swap! b inc))) 21 | (is (= 2 @b)) 22 | (is (= 3 (swap! cd inc))) 23 | (is (= 3 @cd))) 24 | 25 | (testing "reset!" 26 | (is (= 4 (reset! b 4))) 27 | (is (= 4 @b)) 28 | (is (= 5 (reset! cd 5))) 29 | (is (= 5 @cd))) 30 | 31 | (testing "compare-and-set!" 32 | (is (= true (compare-and-set! b 4 6))) 33 | (is (= 6 @b)) 34 | (is (= false (compare-and-set! b 4 7))) 35 | (is (= 6 @b)) 36 | (is (= true (compare-and-set! cd 5 8))) 37 | (is (= 8 @cd)) 38 | (is (= false (compare-and-set! cd 5 9))) 39 | (is (= 8 @cd))) 40 | 41 | (testing "watches" 42 | (let [b-count (atom 0) 43 | b-states (atom []) 44 | _ (add-watch b ::count (fn [_ _ _ _] (swap! b-count inc))) 45 | _ (add-watch b ::count (fn [_ _ _ _] (swap! b-count inc))) ;; duplicate add 46 | _ (add-watch b ::states (fn [_ _ o n] (swap! b-states conj [o n]))) 47 | cd-count (atom 0) 48 | _ (add-watch cd ::count (fn [_ _ _ _] (swap! cd-count inc)))] 49 | 50 | (= 2 (count (.getWatches b))) 51 | (= 1 (count (.getWatches cd))) 52 | 53 | (swap! b inc) 54 | (is (= 1 @b-count)) 55 | (is (= [[6 7]] @b-states)) 56 | (is (= 0 @cd-count)) 57 | 58 | (swap! cd inc) 59 | (is (= 1 @b-count)) 60 | (is (= [[6 7]] @b-states)) 61 | (is (= 1 @cd-count)) 62 | 63 | (remove-watch b ::count) 64 | (= 1 (count (.getWatches b))) 65 | (swap! b inc) 66 | (is (= 1 @b-count)) 67 | (is (= [[6 7] [7 8]] @b-states))))) 68 | 69 | (testing "meta" 70 | (let [c (prum/cursor (atom nil) :b :meta {:k 1})] 71 | (is (= {:k 1} (meta c))) 72 | (alter-meta! c update :k inc) 73 | (is (= {:k 2} (meta c))) 74 | (reset-meta! c {:l 3}) 75 | (is (= {:l 3} (meta c))))) 76 | 77 | (testing "vectors" 78 | (let [a (atom [1 [2 3] {:k 4}]) 79 | b (prum/cursor a 0) 80 | c (prum/cursor-in a [1 0]) 81 | d (prum/cursor-in a [2 :k])] 82 | (is (= 1 @b)) 83 | (is (= 2 @c)) 84 | (is (= 4 @d)) 85 | (swap! b inc) 86 | (swap! c inc) 87 | (swap! d inc) 88 | (is (= [2 [3 3] {:k 5}] @a))))) 89 | 90 | -------------------------------------------------------------------------------- /test/prum/test/defc.clj: -------------------------------------------------------------------------------- 1 | (ns prum.test.defc 2 | (:require 3 | [prum.core] 4 | [clojure.string :as str] 5 | [clojure.test :refer [deftest is are testing]] 6 | [clojure.java.shell :as shell])) 7 | 8 | (defmacro eval-in-temp-ns [& forms] 9 | `(binding [*ns* *ns*] 10 | (in-ns (gensym)) 11 | (clojure.core/use 'clojure.core) 12 | (clojure.core/use 'prum.core) 13 | (eval 14 | '(do ~@forms)))) 15 | 16 | ;; Copied from Clojure: https://git.io/vwFsG 17 | (deftest defc-error-messages 18 | (testing "bad name" 19 | (is (thrown-with-msg? 20 | IllegalArgumentException 21 | #"First argument to defc must be a symbol" 22 | (eval-in-temp-ns (defc "bad docstring" testname [arg1 arg2]))))) 23 | (testing "mixins after argvec" 24 | (is (thrown-with-msg? 25 | IllegalArgumentException 26 | #"Mixins must be given before argument list" 27 | (eval-in-temp-ns (defc testname "docstring" [arg1 arg2] < misplaced-mixin)))) 28 | (is (thrown-with-msg? 29 | IllegalArgumentException 30 | #"Mixins must be given before argument list" 31 | (eval-in-temp-ns (defc testname "docstring" 32 | ([arg1] < misplaced-mixin) 33 | ([arg1 arg2] < misplaced-mixin))))) 34 | (is (thrown-with-msg? 35 | IllegalArgumentException 36 | #"Mixins must be given before argument list" 37 | (eval-in-temp-ns (defc testname 38 | ([arg1] < misplaced-mixin) 39 | ([arg1 arg2] < misplaced-mixin))))))) 40 | 41 | (deftest defc-conditions 42 | (testing "no conditions supplied" 43 | (is (= '(def pre-post-test 44 | (prum.core/build-defc 45 | (clojure.core/fn 46 | ([y] (do {:x 1})) 47 | ([y z] (do (sablono.interpreter/interpret (+ y z 1))))) 48 | nil 49 | "pre-post-test")) 50 | (#'prum.core/-defc 'prum.core/build-defc 51 | true ; cljs? 52 | '(pre-post-test ([y] {:x 1}) 53 | ([y z] (+ y z 1))))))) 54 | (testing "some conditions supplied" 55 | (is (= '(def pre-post-test 56 | (prum.core/build-defc 57 | (clojure.core/fn 58 | ([y] {:pre [(pos? y)]} (do {:x 1})) 59 | ([y z] (do (sablono.interpreter/interpret (+ y z 1))))) 60 | nil 61 | "pre-post-test")) 62 | (#'prum.core/-defc 'prum.core/build-defc 63 | true ; cljs? 64 | '(pre-post-test ([y] {:pre [(pos? y)]} {:x 1}) 65 | ([y z] (+ y z 1)))))))) 66 | -------------------------------------------------------------------------------- /perf/prum/perf.clj: -------------------------------------------------------------------------------- 1 | (ns prum.perf 2 | (:require 3 | [prum.core :as prum] 4 | [clojure.string :as str] 5 | [net.cgrand.enlive-html :as enlive] 6 | [clojure.test :refer [deftest]] 7 | [criterium.core :as criterium] 8 | [hiccup.core :as hiccup])) 9 | 10 | 11 | (def ^:dynamic *convert-style?* true) 12 | 13 | 14 | (defn convert-tag-name [tag attrs] 15 | (let [id (:id attrs) 16 | classes (when-not (str/blank? (:class attrs)) 17 | (->> (str/split (:class attrs) #"\s+") 18 | (remove str/blank?)))] 19 | (keyword 20 | (str tag 21 | (when id (str "#" id)) 22 | (when-not (empty? classes) 23 | (str "." (str/join "." classes))))))) 24 | 25 | 26 | (defn convert-style [s] 27 | (into {} 28 | (for [[_ k v] (re-seq #"([\w+\-]+)\s*:\s*([^;]+)" s)] 29 | (let [k' (keyword k) 30 | v' (condp re-matches v 31 | #"(\d+)px" :>> (fn [[_ n]] (Long/parseLong n)) 32 | #"(\d+\.\d+)px" :>> (fn [[_ n]] (Double/parseDouble n)) 33 | v)] 34 | [k' v'])))) 35 | 36 | 37 | (defn convert-attrs [attrs] 38 | (cond-> attrs 39 | true (dissoc :class :id :data-bem) 40 | (and *convert-style?* 41 | (contains? attrs :style)) (update :style convert-style) 42 | true not-empty)) 43 | 44 | 45 | (defn convert-tag [form] 46 | (cond 47 | ;; tag 48 | (map? form) 49 | (if (= :comment (:type form)) 50 | nil 51 | (let [{:keys [tag attrs content type]} form 52 | tag' (convert-tag-name (name tag) attrs) 53 | attrs' (convert-attrs attrs) 54 | children (->> (map convert-tag content) 55 | (remove nil?))] 56 | (vec 57 | (concat [tag'] (when attrs' [attrs']) children)))) 58 | 59 | ;; text node 60 | (string? form) 61 | (if (str/blank? form) nil form))) 62 | 63 | 64 | (defn convert-page [page] 65 | (-> (slurp page) 66 | .getBytes 67 | java.io.ByteArrayInputStream. 68 | enlive/html-resource 69 | (enlive/select [:body]) 70 | first 71 | convert-tag)) 72 | 73 | 74 | (defn file-size [path] 75 | (-> (count (slurp path)) (/ 1000) (long) (str " kB"))) 76 | 77 | 78 | (defn -main [& args] 79 | (doseq [page ["page1.html" 80 | "page2.html" 81 | "page3.html"] 82 | :let [path (str "perf/pages/" page)]] 83 | (let [comp (convert-page path)] 84 | (println "\n--- Testing" page (str "(" (file-size path) ")") "---") 85 | (criterium/quick-bench (prum/render-static-markup comp))) 86 | 87 | (let [comp (binding [*convert-style?* false] 88 | (convert-page path))] 89 | (println "\n+++ With Hiccup +++") 90 | (criterium/quick-bench (hiccup/html comp))))) 91 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.roman01la/prum "0.10.8-11" 2 | :description "ClojureScript wrapper for Preact" 3 | :license {:name "Eclipse" 4 | :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | :url "https://github.com/roman01la/prum" 6 | 7 | :dependencies [[org.clojure/clojure "1.9.0"] 8 | [org.clojure/clojurescript "1.9.946"] 9 | [org.roman01la/cljss "1.5.13"] 10 | [sablono "0.8.1" 11 | :exclusions [org.clojure/clojure 12 | org.omcljs/om cljsjs/react 13 | cljsjs/react-dom 14 | cljsjs/react-dom-server]] 15 | [hicada "0.1"]] 16 | 17 | :global-vars 18 | {*warn-on-reflection* true} 19 | 20 | :plugins [[lein-cljsbuild "1.1.7"]] 21 | 22 | :profiles {:dev {:source-paths ["examples"] 23 | :dependencies [[clj-diffmatchpatch "0.0.9.3" :exclusions [org.clojure/clojure]]]} 24 | :perf {:source-paths ["perf"] 25 | :dependencies 26 | [[enlive "1.1.6"] 27 | [criterium "0.4.4"] 28 | [hiccup "1.0.5"]]}} 29 | 30 | :aliases {"package" ["do" ["clean"] ["test"] ["clean"] ["cljsbuild" "once" "advanced"] ["run" "-m" "rum.examples-page"]] 31 | "perf" ["with-profile" "perf" "run" "-m" "rum.perf"]} 32 | 33 | 34 | :cljsbuild 35 | {:builds 36 | [{:id "advanced" 37 | :source-paths ["src" "examples" "test"] 38 | :compiler 39 | {:main prum.examples 40 | :output-to "target/main.js" 41 | :optimizations :advanced 42 | :source-map "target/main.js.map" 43 | :pretty-print false 44 | :compiler-stats true 45 | :parallel-build true 46 | :install-deps true 47 | :npm-deps {"prum-preact" "8.2.5" 48 | "preact-render-to-string" "3.6.3"}}} 49 | 50 | {:id "none" 51 | :source-paths ["src" "examples" "test"] 52 | :compiler 53 | {:main prum.examples 54 | :output-to "target/main.js" 55 | :output-dir "target/none" 56 | :asset-path "target/none" 57 | :optimizations :none 58 | :source-map true 59 | :compiler-stats true 60 | :parallel-build true 61 | :install-deps true 62 | :npm-deps {"prum-preact" "8.2.5" 63 | "preact-render-to-string" "3.6.3"}}} 64 | 65 | {:id "test" 66 | :source-paths ["src" "test"] 67 | :compiler 68 | {:main prum.test.server-render 69 | :output-to "target/test.js" 70 | :output-dir "target/test" 71 | :asset-path "target/test" 72 | :optimizations :advanced 73 | :pretty-print true 74 | :pseudo-names true 75 | :parallel-build true 76 | :install-deps true 77 | :npm-deps {"prum-preact" "8.2.5" 78 | "preact-render-to-string" "3.6.3"}}}]}) 79 | 80 | -------------------------------------------------------------------------------- /examples/prum/examples/swap_recognizer.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.examples.swap-recognizer 2 | (:require [prum.core :as prum] 3 | [goog.object :as gobj])) 4 | 5 | (def tolerance 100) 6 | 7 | (def initial-state {:x [] :y [] :match nil}) 8 | 9 | (defn- capture [state e] 10 | (let [touch (-> e (gobj/get "touches") (gobj/get "0"))] 11 | (swap! state #(-> % 12 | (update :x conj (.-clientX touch)) 13 | (update :y conj (.-clientY touch)))))) 14 | 15 | (defn- compute [on-swipe state e] 16 | (let [{:keys [x y]} @state 17 | x0 (first x) 18 | y0 (first y) 19 | x1 (last x) 20 | y1 (last y) 21 | xdt (- x1 x0) 22 | ydt (- y1 y0) 23 | gesture (cond 24 | (and (> y0 y1) 25 | (>= (Math/abs ydt) tolerance) 26 | (< xdt tolerance)) 27 | :up 28 | 29 | (and (< y0 y1) 30 | (>= ydt tolerance) 31 | (< xdt tolerance)) 32 | :down 33 | 34 | (and (> x0 x1) 35 | (>= (Math/abs xdt) tolerance) 36 | (< ydt tolerance)) 37 | :left 38 | 39 | (and (< x0 x1) 40 | (>= xdt tolerance) 41 | (< ydt tolerance)) 42 | :right 43 | 44 | :else nil)] 45 | (when gesture 46 | (on-swipe gesture)) 47 | (reset! state initial-state))) 48 | 49 | (defn swap-recognizer-mixin [f] 50 | {:did-mount 51 | (fn [st] 52 | (let [node (prum/dom-node st) 53 | state (atom initial-state) 54 | set-state #(f (::state st) %) 55 | capture #(capture state %) 56 | compute #(compute set-state state %)] 57 | (.addEventListener node "touchstart" capture) 58 | (.addEventListener node "touchmove" capture) 59 | (.addEventListener node "touchend" compute) 60 | (-> st 61 | (assoc ::capture-state state) 62 | (assoc ::capture capture) 63 | (assoc ::compute compute)))) 64 | :will-unmount 65 | (fn [st] 66 | (let [node (prum/dom-node st) 67 | state (::capture-state st) 68 | capture (::capture st) 69 | compute (::compute st)] 70 | (.removeEventListener node "touchstart" capture) 71 | (.removeEventListener node "touchmove" capture) 72 | (.removeEventListener node "touchend" compute) 73 | (dissoc st ::capture-state ::capture ::compute)))}) 74 | 75 | (prum/defcs example < 76 | (prum/local 0 ::state) 77 | (swap-recognizer-mixin 78 | #(cond 79 | (and (zero? @%1) (= %2 :right)) nil 80 | (= %2 :left) (swap! %1 inc) 81 | (= %2 :right) (swap! %1 dec) 82 | :else nil)) 83 | [{state ::state}] 84 | (let [idx @state] 85 | [:div {:style {:height 160 86 | :background "#eee" 87 | :position "relative" 88 | :overflow "hidden"}} 89 | [:div {:style {:transform (str "translate3d(" (* -256 idx) "px, 0, 0)") 90 | :display "flex"}} 91 | (for [x (range (inc idx))] 92 | [:div {:key x 93 | :style {:min-width 256 94 | :min-height 160 95 | :display "flex" 96 | :align-items "center" 97 | :justify-content "center"}} 98 | x])]])) 99 | 100 | 101 | (defn mount! [el] 102 | (prum/mount (example) el)) 103 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Rum test page 7 | 8 | 32 | 33 | 34 | 35 | 36 |

37 |
Timers
38 |
39 |
40 |
41 | 42 |
43 |
Controls
44 |
45 |
46 | 47 |
48 |
Reactive binary clock
49 |
50 |
51 | 52 |
53 |
Reactive artboard
54 |
55 |
56 | 57 |
58 |
BMI Calculator
59 |
60 |
61 | 62 |
63 |
Form validation
64 |
65 |
66 | 67 |
68 |
Inputs
69 |
70 |
71 | 72 |
73 |
Refs
74 |
75 |
76 | 77 |
78 |
Local state
79 |
80 |
81 | 82 |
83 |
Keys
84 |
85 |
86 | 87 |
88 |
Self-reference
89 |
90 |
91 | 92 |
93 |
Contexts
94 |
95 |
96 | 97 |
98 |
Custom Methods and Data
99 |
100 |
101 | 102 |
103 |
Swipe Recognizer
104 |
105 |
106 | 107 |
108 |
Dynamic styles via :css attribute
109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /examples/prum/examples_page.clj: -------------------------------------------------------------------------------- 1 | (ns prum.examples-page 2 | (:require 3 | [prum.core :as prum] 4 | [prum.examples.core :as core] 5 | [prum.examples.timer-reactive :as timer-reactive] 6 | [prum.examples.timer-static :as timer-static] 7 | [prum.examples.controls :as controls] 8 | [prum.examples.binary-clock :as binary-clock] 9 | [prum.examples.board-reactive :as board-reactive] 10 | [prum.examples.bmi-calculator :as bmi-calculator] 11 | [prum.examples.inputs :as inputs] 12 | [prum.examples.refs :as refs] 13 | [prum.examples.local-state :as local-state] 14 | [prum.examples.keys :as keys] 15 | [prum.examples.self-reference :as self-reference])) 16 | 17 | (def page (str 18 | " 19 | 20 | 21 | 22 | Rum test page 23 | 24 | 48 | 49 | 50 | 51 | 52 |
53 |
Timers
54 |
" (prum/render-html (timer-static/timer-static "Static" @core/*clock)) "
55 |
" (prum/render-html (timer-reactive/timer-reactive)) "
56 |
57 | 58 |
59 |
Controls
60 |
" (prum/render-html (controls/controls)) "
61 |
62 | 63 |
64 |
Reactive binary clock
65 |
" (prum/render-html (binary-clock/binary-clock)) "
66 |
67 | 68 |
69 |
Reactive artboard
70 |
" (prum/render-html (board-reactive/board-reactive)) "
71 |
72 | 73 |
74 |
BMI Calculator
75 |
" (prum/render-html (bmi-calculator/bmi-calculator)) "
76 |
77 | 78 |
79 |
Form validation
80 |
81 |
82 | 83 |
84 |
Inputs
85 |
" (prum/render-html (inputs/inputs)) "
86 |
87 | 88 |
89 |
Local state
90 |
" (prum/render-html (local-state/local-state "Clicks count")) "
91 |
92 | 93 |
94 |
Keys
95 |
" (prum/render-html (keys/keys)) "
96 |
97 | 98 |
99 |
Self-reference
100 |
" (prum/render-html (self-reference/self-reference [:a [:b [:c :d [:e] :g]]])) "
101 |
102 | 103 |
104 |
Contexts
105 |
106 |
107 | 108 |
109 |
Custom Methods and Data
110 |
111 |
112 | 113 | 114 | 115 | 116 | ")) 117 | 118 | 119 | (defn -main [& args] 120 | (println "Writing \"index.html\"") 121 | (spit "index.html" page)) 122 | 123 | -------------------------------------------------------------------------------- /src/prum/core.clj: -------------------------------------------------------------------------------- 1 | (ns prum.core 2 | (:refer-clojure :exclude [ref]) 3 | (:require 4 | [prum.compiler :as c] 5 | [prum.cursor :as cursor] 6 | [prum.server-render :as render] 7 | [prum.util :as util :refer [collect collect* call-all]] 8 | [prum.derived-atom :as derived-atom]) 9 | (:import 10 | [prum.cursor Cursor])) 11 | 12 | 13 | (defn- fn-body? [form] 14 | (when (and (seq? form) 15 | (vector? (first form))) 16 | (if (= '< (second form)) 17 | (throw (IllegalArgumentException. "Mixins must be given before argument list")) 18 | true))) 19 | 20 | 21 | (defn- parse-defc 22 | ":name :doc? ReactElement 75 | 5. Defines top-level var with provided name and assigns ctor to it 76 | 77 | (prum/defc label [t] 78 | [:div t]) 79 | 80 | ;; creates React class 81 | ;; defines ctor fn (defn label [t] ...) => element 82 | 83 | (label \"text\") ;; => returns React element built with label class 84 | 85 | Usage: 86 | 87 | (defc name doc-string? [< mixins+]? [params*] render-body+)" 88 | [& body] 89 | (-defc 'prum.core/build-defc (boolean (:ns &env)) body)) 90 | 91 | 92 | (defmacro defcs 93 | "Same as defc, but render will take additional first argument: state 94 | 95 | Usage: 96 | 97 | (defcs name doc-string? [< mixins+]? [state params*] render-body+)" 98 | [& body] 99 | (-defc 'prum.core/build-defcs (boolean (:ns &env)) body)) 100 | 101 | 102 | (defmacro defcc 103 | "Same as defc, but render will take additional first argument: react component 104 | 105 | Usage: 106 | 107 | (defcc name doc-string? [< mixins+]? [comp params*] render-body+)" 108 | [& body] 109 | (-defc 'prum.core/build-defcc (boolean (:ns &env)) body)) 110 | 111 | 112 | (defn- build-ctor [render mixins display-name] 113 | (let [init (collect :init mixins) ;; state props -> state 114 | will-mount (collect* [:will-mount ;; state -> state 115 | :before-render] mixins) ;; state -> state 116 | render render ;; state -> [dom state] 117 | wrapped-render (reduce #(%2 %1) render (collect :wrap-render mixins))] ;; render-fn -> render-fn 118 | (fn [& args] 119 | (let [props nil 120 | state (-> {:prum/args args} 121 | (call-all init props) 122 | (call-all will-mount)) 123 | [dom _] (wrapped-render state)] 124 | (or dom [:prum/nothing]))))) 125 | 126 | 127 | (defn build-defc [render-body mixins display-name] 128 | (if (empty? mixins) 129 | (fn [& args] (or (apply render-body args) [:prum/nothing])) 130 | (let [render (fn [state] [(apply render-body (:prum/args state)) state])] 131 | (build-ctor render mixins display-name)))) 132 | 133 | 134 | (defn build-defcs [render-body mixins display-name] 135 | (let [render (fn [state] [(apply render-body state (:prum/args state)) state])] 136 | (build-ctor render mixins display-name))) 137 | 138 | 139 | (defn build-defcc [render-body mixins display-name] 140 | (let [render (fn [state] [(apply render-body (:prum/react-component state) (:prum/args state)) state])] 141 | (build-ctor render mixins display-name))) 142 | 143 | 144 | ;; prum.core APIs 145 | 146 | 147 | (defn with-key [element key] 148 | (cond 149 | (render/nothing? element) 150 | element 151 | 152 | (map? (get element 1)) 153 | (assoc-in element [1 :key] key) 154 | 155 | :else 156 | (into [(first element) {:key key}] (next element)))) 157 | 158 | 159 | (defn with-ref [element ref] 160 | element) 161 | 162 | (defn use-ref [component key] 163 | key) 164 | 165 | 166 | ;; mixins 167 | 168 | 169 | (def static {}) 170 | 171 | 172 | (defn local 173 | ([initial] (local initial :prum/local)) 174 | ([initial key] 175 | {:will-mount (fn [state] 176 | (assoc state key (atom initial)))})) 177 | 178 | 179 | (def reactive {}) 180 | 181 | 182 | (def react deref) 183 | 184 | 185 | (defn cursor-in 186 | "Given atom with deep nested value and path inside it, creates an atom-like structure 187 | that can be used separately from main atom, but will sync changes both ways: 188 | 189 | (def db (atom { :users { \"Ivan\" { :age 30 }}})) 190 | (def ivan (prum/cursor db [:users \"Ivan\"])) 191 | \\@ivan ;; => { :age 30 } 192 | (swap! ivan update :age inc) ;; => { :age 31 } 193 | \\@db ;; => { :users { \"Ivan\" { :age 31 }}} 194 | (swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}} 195 | \\@ivan ;; => { :age 32 } 196 | 197 | Returned value supports deref, swap!, reset!, watches and metadata. 198 | The only supported option is `:meta`" 199 | ^prum.cursor.Cursor [ref path & {:as options}] 200 | (if (instance? Cursor ref) 201 | (cursor/Cursor. (.-ref ^Cursor ref) (into (.-path ^Cursor ref) path) (:meta options) (volatile! {})) 202 | (cursor/Cursor. ref path (:meta options) (volatile! {})))) 203 | 204 | 205 | (defn cursor 206 | "Same as `prum.core/cursor-in` but accepts single key instead of path vector" 207 | ^prum.cursor.Cursor [ref key & options] 208 | (apply cursor-in ref [key] options)) 209 | 210 | (def ^{:style/indent 2} derived-atom 211 | "Use this to create “chains” and acyclic graphs of dependent atoms. 212 | `derived-atom` will: 213 | - Take N “source” refs 214 | - Set up a watch on each of them 215 | - Create “sink” atom 216 | - When any of source refs changes: 217 | - re-run function `f`, passing N dereferenced values of source refs 218 | - `reset!` result of `f` to the sink atom 219 | - return sink atom 220 | 221 | (def *a (atom 0)) 222 | (def *b (atom 1)) 223 | (def *x (derived-atom [*a *b] ::key 224 | (fn [a b] 225 | (str a \":\" b)))) 226 | (type *x) ;; => clojure.lang.Atom 227 | \\@*x ;; => 0:1 228 | (swap! *a inc) 229 | \\@*x ;; => 1:1 230 | (reset! *b 7) 231 | \\@*x ;; => 1:7 232 | 233 | Arguments: 234 | refs - sequence of source refs 235 | key - unique key to register watcher, see `clojure.core/add-watch` 236 | f - function that must accept N arguments (same as number of source refs) 237 | and return a value to be written to the sink ref. 238 | Note: `f` will be called with already dereferenced values 239 | opts - optional. Map of: 240 | :ref - Use this as sink ref. By default creates new atom 241 | :check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`. 242 | If result of `f` is equal to the old one, do not call `reset!`. 243 | Defaults to `true`. Set to false if calling `=` would be expensive" 244 | derived-atom/derived-atom) 245 | 246 | 247 | ;;; Server-side rendering 248 | 249 | 250 | (def render-html 251 | "Main server-side rendering method. Given component, returns HTML string with 252 | static markup of that component. Serve that string to the browser and 253 | `prum.core/mount` same Rum component over it. React will be able to reuse already 254 | existing DOM and will initialize much faster" 255 | render/render-html) 256 | 257 | (def render-static-markup 258 | "Same as `prum.core/render-html` but returned string has nothing React-specific. 259 | This allows Rum to be used as traditional server-side template engine" 260 | render/render-static-markup) 261 | 262 | 263 | ;; method parity with CLJS version so you can avoid conditional directive 264 | ;; in e.g. did-mount/will-unmount mixin bodies 265 | 266 | 267 | (defn state [c] 268 | (throw (UnsupportedOperationException. "state is only available from ClojureScript"))) 269 | 270 | 271 | (defn dom-node [s] 272 | (throw (UnsupportedOperationException. "dom-node is only available from ClojureScript"))) 273 | 274 | 275 | (defn ref [s k] 276 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript"))) 277 | 278 | 279 | (defn ref-node [s k] 280 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript"))) 281 | 282 | 283 | (defn context [component key] 284 | (throw (UnsupportedOperationException. "context is only available from ClojureScript"))) 285 | 286 | 287 | (defn mount [c n] 288 | (throw (UnsupportedOperationException. "mount is only available from ClojureScript"))) 289 | 290 | 291 | (defn unmount [c] 292 | (throw (UnsupportedOperationException. "unmount is only available from ClojureScript"))) 293 | 294 | 295 | (defn request-render [c] 296 | (throw (UnsupportedOperationException. "request-render is only available from ClojureScript"))) 297 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | i) changes to the Program, and 15 | ii) additions to the Program; 16 | 17 | where such changes and/or additions to the Program originate from and are 18 | distributed by that particular Contributor. A Contribution 'originates' 19 | from a Contributor if it was added to the Program by such Contributor 20 | itself or anyone acting on such Contributor's behalf. Contributions do not 21 | include additions to the Program which: (i) are separate modules of 22 | software distributed in conjunction with the Program under their own 23 | license agreement, and (ii) are not derivative works of the Program. 24 | 25 | "Contributor" means any person or entity that distributes the Program. 26 | 27 | "Licensed Patents" mean patent claims licensable by a Contributor which are 28 | necessarily infringed by the use or sale of its Contribution alone or when 29 | combined with the Program. 30 | 31 | "Program" means the Contributions distributed in accordance with this 32 | Agreement. 33 | 34 | "Recipient" means anyone who receives the Program under this Agreement, 35 | including all Contributors. 36 | 37 | 2. GRANT OF RIGHTS 38 | a) Subject to the terms of this Agreement, each Contributor hereby grants 39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 40 | reproduce, prepare derivative works of, publicly display, publicly 41 | perform, distribute and sublicense the Contribution of such Contributor, 42 | if any, and such derivative works, in source code and object code form. 43 | b) Subject to the terms of this Agreement, each Contributor hereby grants 44 | Recipient a non-exclusive, worldwide, royalty-free patent license under 45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 46 | transfer the Contribution of such Contributor, if any, in source code and 47 | object code form. This patent license shall apply to the combination of 48 | the Contribution and the Program if, at the time the Contribution is 49 | added by the Contributor, such addition of the Contribution causes such 50 | combination to be covered by the Licensed Patents. The patent license 51 | shall not apply to any other combinations which include the Contribution. 52 | No hardware per se is licensed hereunder. 53 | c) Recipient understands that although each Contributor grants the licenses 54 | to its Contributions set forth herein, no assurances are provided by any 55 | Contributor that the Program does not infringe the patent or other 56 | intellectual property rights of any other entity. Each Contributor 57 | disclaims any liability to Recipient for claims brought by any other 58 | entity based on infringement of intellectual property rights or 59 | otherwise. As a condition to exercising the rights and licenses granted 60 | hereunder, each Recipient hereby assumes sole responsibility to secure 61 | any other intellectual property rights needed, if any. For example, if a 62 | third party patent license is required to allow Recipient to distribute 63 | the Program, it is Recipient's responsibility to acquire that license 64 | before distributing the Program. 65 | d) Each Contributor represents that to its knowledge it has sufficient 66 | copyright rights in its Contribution, if any, to grant the copyright 67 | license set forth in this Agreement. 68 | 69 | 3. REQUIREMENTS 70 | 71 | A Contributor may choose to distribute the Program in object code form under 72 | its own license agreement, provided that: 73 | 74 | a) it complies with the terms and conditions of this Agreement; and 75 | b) its license agreement: 76 | i) effectively disclaims on behalf of all Contributors all warranties 77 | and conditions, express and implied, including warranties or 78 | conditions of title and non-infringement, and implied warranties or 79 | conditions of merchantability and fitness for a particular purpose; 80 | ii) effectively excludes on behalf of all Contributors all liability for 81 | damages, including direct, indirect, special, incidental and 82 | consequential damages, such as lost profits; 83 | iii) states that any provisions which differ from this Agreement are 84 | offered by that Contributor alone and not by any other party; and 85 | iv) states that source code for the Program is available from such 86 | Contributor, and informs licensees how to obtain it in a reasonable 87 | manner on or through a medium customarily used for software exchange. 88 | 89 | When the Program is made available in source code form: 90 | 91 | a) it must be made available under this Agreement; and 92 | b) a copy of this Agreement must be included with each copy of the Program. 93 | Contributors may not remove or alter any copyright notices contained 94 | within the Program. 95 | 96 | Each Contributor must identify itself as the originator of its Contribution, 97 | if 98 | any, in a manner that reasonably allows subsequent Recipients to identify the 99 | originator of the Contribution. 100 | 101 | 4. COMMERCIAL DISTRIBUTION 102 | 103 | Commercial distributors of software may accept certain responsibilities with 104 | respect to end users, business partners and the like. While this license is 105 | intended to facilitate the commercial use of the Program, the Contributor who 106 | includes the Program in a commercial product offering should do so in a manner 107 | which does not create potential liability for other Contributors. Therefore, 108 | if a Contributor includes the Program in a commercial product offering, such 109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 110 | every other Contributor ("Indemnified Contributor") against any losses, 111 | damages and costs (collectively "Losses") arising from claims, lawsuits and 112 | other legal actions brought by a third party against the Indemnified 113 | Contributor to the extent caused by the acts or omissions of such Commercial 114 | Contributor in connection with its distribution of the Program in a commercial 115 | product offering. The obligations in this section do not apply to any claims 116 | or Losses relating to any actual or alleged intellectual property 117 | infringement. In order to qualify, an Indemnified Contributor must: 118 | a) promptly notify the Commercial Contributor in writing of such claim, and 119 | b) allow the Commercial Contributor to control, and cooperate with the 120 | Commercial Contributor in, the defense and any related settlement 121 | negotiations. The Indemnified Contributor may participate in any such claim at 122 | its own expense. 123 | 124 | For example, a Contributor might include the Program in a commercial product 125 | offering, Product X. That Contributor is then a Commercial Contributor. If 126 | that Commercial Contributor then makes performance claims, or offers 127 | warranties related to Product X, those performance claims and warranties are 128 | such Commercial Contributor's responsibility alone. Under this section, the 129 | Commercial Contributor would have to defend claims against the other 130 | Contributors related to those performance claims and warranties, and if a 131 | court requires any other Contributor to pay any damages as a result, the 132 | Commercial Contributor must pay those damages. 133 | 134 | 5. NO WARRANTY 135 | 136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 140 | Recipient is solely responsible for determining the appropriateness of using 141 | and distributing the Program and assumes all risks associated with its 142 | exercise of rights under this Agreement , including but not limited to the 143 | risks and costs of program errors, compliance with applicable laws, damage to 144 | or loss of data, programs or equipment, and unavailability or interruption of 145 | operations. 146 | 147 | 6. DISCLAIMER OF LIABILITY 148 | 149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 156 | OF SUCH DAMAGES. 157 | 158 | 7. GENERAL 159 | 160 | If any provision of this Agreement is invalid or unenforceable under 161 | applicable law, it shall not affect the validity or enforceability of the 162 | remainder of the terms of this Agreement, and without further action by the 163 | parties hereto, such provision shall be reformed to the minimum extent 164 | necessary to make such provision valid and enforceable. 165 | 166 | If Recipient institutes patent litigation against any entity (including a 167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 168 | (excluding combinations of the Program with other software or hardware) 169 | infringes such Recipient's patent(s), then such Recipient's rights granted 170 | under Section 2(b) shall terminate as of the date such litigation is filed. 171 | 172 | All Recipient's rights under this Agreement shall terminate if it fails to 173 | comply with any of the material terms or conditions of this Agreement and does 174 | not cure such failure in a reasonable period of time after becoming aware of 175 | such noncompliance. If all Recipient's rights under this Agreement terminate, 176 | Recipient agrees to cease use and distribution of the Program as soon as 177 | reasonably practicable. However, Recipient's obligations under this Agreement 178 | and any licenses granted by Recipient relating to the Program shall continue 179 | and survive. 180 | 181 | Everyone is permitted to copy and distribute copies of this Agreement, but in 182 | order to avoid inconsistency the Agreement is copyrighted and may only be 183 | modified in the following manner. The Agreement Steward reserves the right to 184 | publish new versions (including revisions) of this Agreement from time to 185 | time. No one other than the Agreement Steward has the right to modify this 186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 187 | Eclipse Foundation may assign the responsibility to serve as the Agreement 188 | Steward to a suitable separate entity. Each new version of the Agreement will 189 | be given a distinguishing version number. The Program (including 190 | Contributions) may always be distributed subject to the version of the 191 | Agreement under which it was received. In addition, after a new version of the 192 | Agreement is published, Contributor may elect to distribute the Program 193 | (including its Contributions) under the new version. Except as expressly 194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 195 | licenses to the intellectual property of any Contributor under this Agreement, 196 | whether expressly, by implication, estoppel or otherwise. All rights in the 197 | Program not expressly granted under this Agreement are reserved. 198 | 199 | This Agreement is governed by the laws of the State of New York and the 200 | intellectual property laws of the United States of America. No party to this 201 | Agreement will bring a legal action under this Agreement more than one year 202 | after the cause of action arose. Each party waives its rights to a jury trial in 203 | any resulting litigation. 204 | -------------------------------------------------------------------------------- /src/prum/core.cljs: -------------------------------------------------------------------------------- 1 | (ns prum.core 2 | (:refer-clojure :exclude [ref]) 3 | (:require-macros prum.core) 4 | (:require 5 | ["prum-preact" :as p] 6 | [goog.object :as gobj] 7 | [goog.dom :as gdom] 8 | [goog.functions :as gf] 9 | [prum.cursor :as cursor] 10 | [prum.util :as util :refer [collect collect* call-all]] 11 | [prum.derived-atom :as derived-atom])) 12 | 13 | (defn state 14 | "Given React component, returns Rum state associated with it" 15 | [comp] 16 | (gobj/get (.-state comp) ":prum/state")) 17 | 18 | (defn extend! [obj props] 19 | (doseq [[k v] props 20 | :when (some? v)] 21 | (gobj/set obj (name k) (clj->js v)))) 22 | 23 | 24 | (defn- build-class [render mixins display-name] 25 | (let [init (collect :init mixins) ;; state props -> state 26 | will-mount (collect* [:will-mount ;; state -> state 27 | :before-render] mixins) ;; state -> state 28 | render render ;; state -> [dom state] 29 | wrap-render (collect :wrap-render mixins) ;; render-fn -> render-fn 30 | wrapped-render (reduce #(%2 %1) render wrap-render) 31 | did-mount (collect* [:did-mount ;; state -> state 32 | :after-render] mixins) ;; state -> state 33 | did-remount (collect :did-remount mixins) ;; old-state state -> state 34 | should-update (collect :should-update mixins) ;; old-state state -> boolean 35 | will-update (collect* [:will-update ;; state -> state 36 | :before-render] mixins) ;; state -> state 37 | did-update (collect* [:did-update ;; state -> state 38 | :after-render] mixins) ;; state -> state 39 | will-unmount (collect :will-unmount mixins) ;; state -> state 40 | child-context (collect :child-context mixins) ;; state -> child-context 41 | class-props (reduce merge (collect :class-properties mixins)) ;; custom properties and methods 42 | static-props (reduce merge (collect :static-properties mixins)) ;; custom static properties and methods 43 | 44 | ctor (fn [props] 45 | (this-as this 46 | (set! (.. this -state) 47 | #js {":prum/state" 48 | (-> (gobj/get props ":prum/initial-state") 49 | (assoc :prum/react-component this) 50 | (call-all init props) 51 | volatile!)}) 52 | (set! (.. this -refs) {}) 53 | (.call p/Component this props))) 54 | _ (goog/inherits ctor p/Component) 55 | prototype (gobj/get ctor "prototype")] 56 | 57 | (when-not (empty? will-mount) 58 | (set! (.. prototype -componentWillMount) 59 | (fn [] 60 | (this-as this 61 | (vswap! (state this) call-all will-mount))))) 62 | 63 | (when-not (empty? did-mount) 64 | (set! (.. prototype -componentDidMount) 65 | (fn [] 66 | (this-as this 67 | (vswap! (state this) call-all did-mount))))) 68 | 69 | (set! (.. prototype -componentWillReceiveProps) 70 | (fn [next-props] 71 | (this-as this 72 | (let [old-state @(state this) 73 | state (merge old-state 74 | (gobj/get next-props ":prum/initial-state")) 75 | next-state (reduce #(%2 old-state %1) state did-remount)] 76 | ;; allocate new volatile so that we can access both old and new states in shouldComponentUpdate 77 | (.setState this #js {":prum/state" (volatile! next-state)}))))) 78 | 79 | (when-not (empty? should-update) 80 | (set! (.. prototype -shouldComponentUpdate) 81 | (fn [next-props next-state] 82 | (this-as this 83 | (let [old-state @(state this) 84 | new-state @(gobj/get next-state ":prum/state")] 85 | (or (some #(% old-state new-state) should-update) false)))))) 86 | 87 | (when-not (empty? will-update) 88 | (set! (.. prototype -componentWillUpdate) 89 | (fn [_ next-state] 90 | (this-as this 91 | (let [new-state (gobj/get next-state ":prum/state")] 92 | (vswap! new-state call-all will-update)))))) 93 | 94 | (set! (.. prototype -render) 95 | (fn [] 96 | (this-as this 97 | (let [state (state this) 98 | [dom next-state] (wrapped-render @state)] 99 | (vreset! state next-state) 100 | dom)))) 101 | 102 | (when-not (empty? did-update) 103 | (set! (.. prototype -componentDidUpdate) 104 | (fn [_ _] 105 | (this-as this 106 | (vswap! (state this) call-all did-update))))) 107 | 108 | (set! (.. prototype -componentWillUnmount) 109 | (fn [] 110 | (this-as this 111 | (when-not (empty? will-unmount) 112 | (vswap! (state this) call-all will-unmount)) 113 | (gobj/set this ":prum/unmounted?" true)))) 114 | 115 | (when-not (empty? child-context) 116 | (set! (.. prototype -getChildContext) 117 | (fn [] 118 | (this-as this 119 | (let [state @(state this)] 120 | (clj->js (transduce (map #(% state)) merge {} child-context))))))) 121 | 122 | (extend! prototype class-props) 123 | (set! (.. ctor -displayName) display-name) 124 | (extend! ctor static-props) 125 | ctor)) 126 | 127 | 128 | (defn- build-ctor [render mixins display-name] 129 | (let [class (build-class render mixins display-name) 130 | key-fn (first (collect :key-fn mixins)) 131 | ctor (if (some? key-fn) 132 | (fn [& args] 133 | (let [props #js {":prum/initial-state" {:prum/args args} 134 | :key (apply key-fn args)}] 135 | (p/createElement class props))) 136 | (fn [& args] 137 | (let [props #js {":prum/initial-state" {:prum/args args}}] 138 | (p/createElement class props))))] 139 | (with-meta ctor {:prum/class class}))) 140 | 141 | 142 | (defn build-defc [render-body mixins display-name] 143 | (if (empty? mixins) 144 | (let [class (fn [props] 145 | (apply render-body (gobj/get props ":prum/args"))) 146 | _ (set! (.. class -displayName) display-name) 147 | ctor (fn [& args] 148 | (p/createElement class #js {":prum/args" args}))] 149 | (with-meta ctor {:prum/class class})) 150 | (let [render (fn [state] [(apply render-body (:prum/args state)) state])] 151 | (build-ctor render mixins display-name)))) 152 | 153 | 154 | (defn build-defcs [render-body mixins display-name] 155 | (let [render (fn [state] [(apply render-body state (:prum/args state)) state])] 156 | (build-ctor render mixins display-name))) 157 | 158 | 159 | (defn build-defcc [render-body mixins display-name] 160 | (let [render (fn [state] [(apply render-body (:prum/react-component state) (:prum/args state)) state])] 161 | (build-ctor render mixins display-name))) 162 | 163 | (defn- set-meta [ctor] 164 | (let [f #(let [ctor (ctor)] 165 | (.apply ctor ctor (js-arguments)))] 166 | (specify! f IMeta (-meta [_] (meta ctor))) 167 | f)) 168 | 169 | (defn lazy-component [builder render mixins display-name] 170 | (let [bf #(builder render mixins display-name) 171 | ctor (gf/cacheReturnValue bf)] 172 | (set-meta ctor))) 173 | 174 | 175 | (defn request-render 176 | "Re-render preact component" 177 | [component] 178 | (when-not (gobj/get component ":prum/unmounted?") 179 | (.forceUpdate component))) 180 | 181 | 182 | (defn mount 183 | "Add component to the DOM tree. Idempotent. Subsequent mounts will just update component" 184 | ([component node] 185 | (mount component node nil)) 186 | ([component node root] 187 | (let [root (p/render component node root)] 188 | (gobj/set node ":prum/root-node" root) 189 | root))) 190 | 191 | (defn unmount 192 | "Removes component from the DOM tree" 193 | [node] 194 | (let [root (gobj/get node ":prum/root-node") 195 | parent (when root (gdom/getParentElement root))] 196 | (if (= parent node) 197 | (do 198 | (p/render (p/createElement (constantly nil)) parent root) 199 | true) 200 | false))) 201 | 202 | ;; initialization 203 | 204 | (defn with-key 205 | "Adds React key to component" 206 | [component key] 207 | (p/cloneElement component #js {:key key})) 208 | 209 | (defn with-ref 210 | "Adds React ref to component" 211 | [component ref] 212 | (p/cloneElement component #js {:ref ref})) 213 | 214 | (defn use-ref 215 | "Adds node to component as React ref" 216 | [component key] 217 | (fn [node] 218 | (let [refs (.-refs component)] 219 | (->> (assoc refs key node) 220 | (set! (.. component -refs)))))) 221 | 222 | (defn ref 223 | "Given state and ref handle, returns React component" 224 | [state key] 225 | (-> state :prum/react-component .-refs (get key))) 226 | 227 | (defn ref-node 228 | "Given state and ref handle, returns DOM node associated with ref" 229 | [state key] 230 | (when-let [ref (ref state key)] 231 | (.-base ref))) 232 | 233 | (defn dom-node 234 | "Given state, returns top-level DOM node. Can’t be called during render" 235 | [state] 236 | (.-base (:prum/react-component state))) 237 | 238 | (defn context [component key] 239 | (-> component 240 | .-context 241 | (gobj/get (name key)))) 242 | 243 | 244 | ;; static mixin 245 | 246 | (def static 247 | "Mixin. Will avoid re-render if none of component’s arguments have changed. 248 | Does equality check (=) on all arguments" 249 | {:should-update 250 | (fn [old-state new-state] 251 | (not= (:prum/args old-state) (:prum/args new-state)))}) 252 | 253 | 254 | ;; local mixin 255 | 256 | (defn local 257 | "Mixin constructor. Adds an atom to component’s state that can be used to keep stuff 258 | during component’s lifecycle. Component will be re-rendered if atom’s value changes. 259 | Atom is stored under user-provided key or under `:prum/local` by default" 260 | ([initial] (local initial :prum/local)) 261 | ([initial key] 262 | {:will-mount 263 | (fn [state] 264 | (let [local-state (atom initial) 265 | component (:prum/react-component state)] 266 | (add-watch local-state key 267 | (fn [_ _ _ _] 268 | (request-render component))) 269 | (assoc state key local-state)))})) 270 | 271 | 272 | ;; reactive mixin 273 | 274 | (def ^:private ^:dynamic *reactions*) 275 | 276 | 277 | (def reactive 278 | "Mixin. Works in conjunction with `prum.core/react`" 279 | {:init 280 | (fn [state props] 281 | (assoc state :prum.reactive/key (random-uuid))) 282 | :wrap-render 283 | (fn [render-fn] 284 | (fn [state] 285 | (binding [*reactions* (volatile! #{})] 286 | (let [comp (:prum/react-component state) 287 | old-reactions (:prum.reactive/refs state #{}) 288 | [dom next-state] (render-fn state) 289 | new-reactions @*reactions* 290 | key (:prum.reactive/key state)] 291 | (doseq [ref old-reactions] 292 | (when-not (contains? new-reactions ref) 293 | (remove-watch ref key))) 294 | (doseq [ref new-reactions] 295 | (when-not (contains? old-reactions ref) 296 | (add-watch ref key 297 | (fn [_ _ _ _] 298 | (request-render comp))))) 299 | [dom (assoc next-state :prum.reactive/refs new-reactions)])))) 300 | :will-unmount 301 | (fn [state] 302 | (let [key (:prum.reactive/key state)] 303 | (doseq [ref (:prum.reactive/refs state)] 304 | (remove-watch ref key))) 305 | (dissoc state :prum.reactive/refs :prum.reactive/key))}) 306 | 307 | 308 | (defn react 309 | "Works in conjunction with `prum.core/reactive` mixin. Use this function instead of 310 | `deref` inside render, and your component will subscribe to changes happening 311 | to the derefed atom." 312 | [ref] 313 | (assert *reactions* "prum.core/react is only supported in conjunction with prum.core/reactive") 314 | (vswap! *reactions* conj ref) 315 | @ref) 316 | 317 | 318 | ;; derived-atom 319 | 320 | (def ^{:style/indent 2} derived-atom 321 | "Use this to create “chains” and acyclic graphs of dependent atoms. 322 | `derived-atom` will: 323 | - Take N “source” refs 324 | - Set up a watch on each of them 325 | - Create “sink” atom 326 | - When any of source refs changes: 327 | - re-run function `f`, passing N dereferenced values of source refs 328 | - `reset!` result of `f` to the sink atom 329 | - return sink atom 330 | 331 | (def *a (atom 0)) 332 | (def *b (atom 1)) 333 | (def *x (derived-atom [*a *b] ::key 334 | (fn [a b] 335 | (str a \":\" b)))) 336 | (type *x) ;; => clojure.lang.Atom 337 | \\@*x ;; => 0:1 338 | (swap! *a inc) 339 | \\@*x ;; => 1:1 340 | (reset! *b 7) 341 | \\@*x ;; => 1:7 342 | 343 | Arguments: 344 | refs - sequence of source refs 345 | key - unique key to register watcher, see `clojure.core/add-watch` 346 | f - function that must accept N arguments (same as number of source refs) 347 | and return a value to be written to the sink ref. 348 | Note: `f` will be called with already dereferenced values 349 | opts - optional. Map of: 350 | :ref - Use this as sink ref. By default creates new atom 351 | :check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`. 352 | If result of `f` is equal to the old one, do not call `reset!`. 353 | Defaults to `true`. Set to false if calling `=` would be expensive" 354 | derived-atom/derived-atom) 355 | 356 | 357 | ;; cursors 358 | 359 | (defn cursor-in 360 | "Given atom with deep nested value and path inside it, creates an atom-like structure 361 | that can be used separately from main atom, but will sync changes both ways: 362 | 363 | (def db (atom { :users { \"Ivan\" { :age 30 }}})) 364 | (def ivan (prum/cursor db [:users \"Ivan\"])) 365 | \\@ivan ;; => { :age 30 } 366 | (swap! ivan update :age inc) ;; => { :age 31 } 367 | \\@db ;; => { :users { \"Ivan\" { :age 31 }}} 368 | (swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}} 369 | \\@ivan ;; => { :age 32 } 370 | 371 | Returned value supports deref, swap!, reset!, watches and metadata. 372 | The only supported option is `:meta`" 373 | [ref path & {:as options}] 374 | (if (instance? cursor/Cursor ref) 375 | (cursor/Cursor. (gobj/get ref "ref") (into (gobj/get ref "path") path) (:meta options)) 376 | (cursor/Cursor. ref path (:meta options)))) 377 | 378 | 379 | (defn cursor 380 | "Same as `prum.core/cursor-in` but accepts single key instead of path vector" 381 | [ref key & options] 382 | (apply cursor-in ref [key] options)) 383 | -------------------------------------------------------------------------------- /src/prum/server_render.clj: -------------------------------------------------------------------------------- 1 | (ns prum.server-render 2 | (:require 3 | [clojure.string :as str]) 4 | (:import 5 | [clojure.lang IPersistentVector ISeq Named Numbers Ratio Keyword])) 6 | 7 | 8 | (defn nothing? [element] 9 | (and (vector? element) 10 | (= :prum/nothing (first element)))) 11 | 12 | 13 | (def ^:dynamic *select-value*) 14 | 15 | (defn append! 16 | ([^StringBuilder sb s0] (.append sb s0)) 17 | ([^StringBuilder sb s0 s1] 18 | (.append sb s0) 19 | (.append sb s1)) 20 | ([^StringBuilder sb s0 s1 s2] 21 | (.append sb s0) 22 | (.append sb s1) 23 | (.append sb s2)) 24 | ([^StringBuilder sb s0 s1 s2 s3] 25 | (.append sb s0) 26 | (.append sb s1) 27 | (.append sb s2) 28 | (.append sb s3)) 29 | ([^StringBuilder sb s0 s1 s2 s3 s4] 30 | (.append sb s0) 31 | (.append sb s1) 32 | (.append sb s2) 33 | (.append sb s3) 34 | (.append sb s4))) 35 | 36 | 37 | (defprotocol ToString 38 | (^String to-str [x] "Convert a value into a string.")) 39 | 40 | 41 | (extend-protocol ToString 42 | Keyword (to-str [k] (name k)) 43 | Ratio (to-str [r] (str (float r))) 44 | String (to-str [s] s) 45 | Object (to-str [x] (str x)) 46 | nil (to-str [_] "")) 47 | 48 | 49 | (def ^{:doc "A list of elements that must be rendered without a closing tag." 50 | :private true} 51 | void-tags 52 | #{"area" "base" "br" "col" "command" "embed" "hr" "img" "input" "keygen" "link" 53 | "meta" "param" "source" "track" "wbr"}) 54 | 55 | 56 | 57 | (def normalized-attrs 58 | {;; special cases 59 | :default-checked "checked" 60 | :default-value "value" 61 | 62 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/HTMLDOMPropertyConfig.js 63 | :accept-charset "accept-charset" 64 | :access-key "accesskey" 65 | :allow-full-screen "allowfullscreen" 66 | :allow-transparency "allowtransparency" 67 | :auto-complete "autocomplete" 68 | :auto-play "autoplay" 69 | :cell-padding "cellpadding" 70 | :cell-spacing "cellspacing" 71 | :char-set "charset" 72 | :class-id "classid" 73 | :col-span "colspan" 74 | :content-editable "contenteditable" 75 | :context-menu "contextmenu" 76 | :cross-origin "crossorigin" 77 | :date-time "datetime" 78 | :enc-type "enctype" 79 | :form-action "formaction" 80 | :form-enc-type "formenctype" 81 | :form-method "formmethod" 82 | :form-no-validate "formnovalidate" 83 | :form-target "formtarget" 84 | :frame-border "frameborder" 85 | :href-lang "hreflang" 86 | :http-equiv "http-equiv" 87 | :input-mode "inputmode" 88 | :key-params "keyparams" 89 | :key-type "keytype" 90 | :margin-height "marginheight" 91 | :margin-width "marginwidth" 92 | :max-length "maxlength" 93 | :media-group "mediagroup" 94 | :min-length "minlength" 95 | :no-validate "novalidate" 96 | :radio-group "radiogroup" 97 | :referrer-policy "referrerpolicy" 98 | :read-only "readonly" 99 | :row-span "rowspan" 100 | :spell-check "spellcheck" 101 | :src-doc "srcdoc" 102 | :src-lang "srclang" 103 | :src-set "srcset" 104 | :tab-index "tabindex" 105 | :use-map "usemap" 106 | :auto-capitalize "autocapitalize" 107 | :auto-correct "autocorrect" 108 | :auto-save "autosave" 109 | :item-prop "itemprop" 110 | :item-scope "itemscope" 111 | :item-type "itemtype" 112 | :item-id "itemid" 113 | :item-ref "itemref" 114 | 115 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/SVGDOMPropertyConfig.js 116 | :allow-reorder "allowReorder" 117 | :attribute-name "attributeName" 118 | :attribute-type "attributeType" 119 | :auto-reverse "autoReverse" 120 | :base-frequency "baseFrequency" 121 | :base-profile "baseProfile" 122 | :calc-mode "calcMode" 123 | :clip-path-units "clipPathUnits" 124 | :content-script-type "contentScriptType" 125 | :content-style-type "contentStyleType" 126 | :diffuse-constant "diffuseConstant" 127 | :edge-mode "edgeMode" 128 | :external-resources-required "externalResourcesRequired" 129 | :filter-res "filterRes" 130 | :filter-units "filterUnits" 131 | :glyph-ref "glyphRef" 132 | :gradient-transform "gradientTransform" 133 | :gradient-units "gradientUnits" 134 | :kernel-matrix "kernelMatrix" 135 | :kernel-unit-length "kernelUnitLength" 136 | :key-points "keyPoints" 137 | :key-splines "keySplines" 138 | :key-times "keyTimes" 139 | :length-adjust "lengthAdjust" 140 | :limiting-cone-angle "limitingConeAngle" 141 | :marker-height "markerHeight" 142 | :marker-units "markerUnits" 143 | :marker-width "markerWidth" 144 | :mask-content-units "maskContentUnits" 145 | :mask-units "maskUnits" 146 | :num-octaves "numOctaves" 147 | :path-length "pathLength" 148 | :pattern-content-units "patternContentUnits" 149 | :pattern-transform "patternTransform" 150 | :pattern-units "patternUnits" 151 | :points-at-x "pointsAtX" 152 | :points-at-y "pointsAtY" 153 | :points-at-z "pointsAtZ" 154 | :preserve-alpha "preserveAlpha" 155 | :preserve-aspect-ratio "preserveAspectRatio" 156 | :primitive-units "primitiveUnits" 157 | :ref-x "refX" 158 | :ref-y "refY" 159 | :repeat-count "repeatCount" 160 | :repeat-dur "repeatDur" 161 | :required-extensions "requiredExtensions" 162 | :required-features "requiredFeatures" 163 | :specular-constant "specularConstant" 164 | :specular-exponent "specularExponent" 165 | :spread-method "spreadMethod" 166 | :start-offset "startOffset" 167 | :std-deviation "stdDeviation" 168 | :stitch-tiles "stitchTiles" 169 | :surface-scale "surfaceScale" 170 | :system-language "systemLanguage" 171 | :table-values "tableValues" 172 | :target-x "targetX" 173 | :target-y "targetY" 174 | :view-box "viewBox" 175 | :view-target "viewTarget" 176 | :x-channel-selector "xChannelSelector" 177 | :xlink-actuate "xlink:actuate" 178 | :xlink-arcrole "xlink:arcrole" 179 | :xlink-href "xlink:href" 180 | :xlink-role "xlink:role" 181 | :xlink-show "xlink:show" 182 | :xlink-title "xlink:title" 183 | :xlink-type "xlink:type" 184 | :xml-base "xml:base" 185 | :xmlns-xlink "xmlns:xlink" 186 | :xml-lang "xml:lang" 187 | :xml-space "xml:space" 188 | :y-channel-selector "yChannelSelector" 189 | :zoom-and-pan "zoomAndPan"}) 190 | 191 | 192 | (defn get-value [attrs] 193 | (or (:value attrs) 194 | (:default-value attrs))) 195 | 196 | 197 | (defn normalize-attr-key ^String [key] 198 | (or (normalized-attrs key) 199 | (name key))) 200 | 201 | 202 | (defn escape-html [^String s] 203 | (let [len (count s)] 204 | (loop [^StringBuilder sb nil 205 | i (int 0)] 206 | (if (< i len) 207 | (let [char (.charAt s i) 208 | repl (case char 209 | \& "&" 210 | \< "<" 211 | \> ">" 212 | \" """ 213 | \' "'" 214 | nil)] 215 | (if (nil? repl) 216 | (if (nil? sb) 217 | (recur nil (inc i)) 218 | (recur (doto sb 219 | (.append char)) 220 | (inc i))) 221 | (if (nil? sb) 222 | (recur (doto (StringBuilder.) 223 | (.append s 0 i) 224 | (.append repl)) 225 | (inc i)) 226 | (recur (doto sb 227 | (.append repl)) 228 | (inc i))))) 229 | (if (nil? sb) s (str sb)))))) 230 | 231 | 232 | (defn parse-selector [s] 233 | (loop [matches (re-seq #"([#.])?([^#.]+)" (name s)) 234 | tag "div" 235 | id nil 236 | classes nil] 237 | (if-let [[_ prefix val] (first matches)] 238 | (case prefix 239 | nil (recur (next matches) val id classes) 240 | "#" (recur (next matches) tag val classes) 241 | "." (recur (next matches) tag id (conj (or classes []) val))) 242 | [tag id classes]))) 243 | 244 | 245 | (defn normalize-element [[first second & rest]] 246 | (when-not (or (keyword? first) 247 | (symbol? first) 248 | (string? first)) 249 | (throw (ex-info "Expected a keyword as a tag" {:tag first}))) 250 | (let [[tag tag-id tag-classes] (parse-selector first) 251 | [attrs children] (if (or (map? second) 252 | (nil? second)) 253 | [second rest] 254 | [nil (cons second rest)]) 255 | attrs-classes (:class attrs) 256 | classes (if (and tag-classes attrs-classes) 257 | [tag-classes attrs-classes] 258 | (or tag-classes attrs-classes))] 259 | [tag tag-id classes attrs children])) 260 | 261 | 262 | ;;; render attributes 263 | 264 | 265 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/CSSProperty.js 266 | (def unitless-css-props 267 | (into #{} 268 | (for [key ["animation-iteration-count" "box-flex" "box-flex-group" "box-ordinal-group" "column-count" "flex" "flex-grow" "flex-positive" "flex-shrink" "flex-negative" "flex-order" "grid-row" "grid-column" "font-weight" "line-clamp" "line-height" "opacity" "order" "orphans" "tab-size" "widows" "z-index" "zoom" "fill-opacity" "stop-opacity" "stroke-dashoffset" "stroke-opacity" "stroke-width"] 269 | prefix ["" "-webkit-" "-ms-" "-moz-" "-o-"]] 270 | (str prefix key)))) 271 | 272 | 273 | (defn normalize-css-key [k] 274 | (-> (to-str k) 275 | (str/replace #"[A-Z]" (fn [ch] (str "-" (str/lower-case ch)))) 276 | (str/replace #"^ms-" "-ms-"))) 277 | 278 | 279 | (defn normalize-css-value [key value] 280 | (cond 281 | (contains? unitless-css-props key) 282 | (escape-html (to-str value)) 283 | (number? value) 284 | (str value (when (not= 0 value) "px")) 285 | (and (string? value) 286 | (re-matches #"\s*\d+\s*" value)) 287 | (recur key (-> value str/trim Long/parseLong)) 288 | (and (string? value) 289 | (re-matches #"\s*\d+\.\d+\s*" value)) 290 | (recur key (-> value str/trim Double/parseDouble)) 291 | :else 292 | (escape-html (to-str value)))) 293 | 294 | 295 | (defn render-style-kv! [sb empty? k v] 296 | (if v 297 | (do 298 | (when empty? 299 | (append! sb " style=\"")) 300 | (let [key (normalize-css-key k) 301 | val (normalize-css-value key v)] 302 | (append! sb key ":" val ";")) 303 | false) 304 | empty?)) 305 | 306 | 307 | (defn render-style! [map sb] 308 | (let [empty? (reduce-kv (partial render-style-kv! sb) true map)] 309 | (when-not empty? 310 | (append! sb "\"")))) 311 | 312 | 313 | (defn render-class! [sb first? class] 314 | (cond 315 | (nil? class) 316 | first? 317 | (string? class) 318 | (do 319 | (when-not first? 320 | (append! sb " ")) 321 | (append! sb class) 322 | false) 323 | (or (sequential? class) 324 | (set? class)) 325 | (reduce #(render-class! sb %1 %2) first? class) 326 | :else 327 | (render-class! sb first? (to-str class)))) 328 | 329 | 330 | (defn render-classes! [classes sb] 331 | (when classes 332 | (append! sb " class=\"") 333 | (render-class! sb true classes) 334 | (append! sb "\""))) 335 | 336 | 337 | (defn- render-attr-str! [sb attr value] 338 | (append! sb " " attr "=\"" (escape-html (to-str value)) "\"")) 339 | 340 | 341 | (defn render-attr! [tag key value sb] 342 | (let [attr (normalize-attr-key key)] 343 | (cond 344 | (= "type" attr) :nop ;; rendered manually in render-element! before id 345 | (= "style" attr) (render-style! value sb) 346 | (= "key" attr) :nop 347 | (= "ref" attr) :nop 348 | (= "class" attr) :nop 349 | (and (= "value" attr) 350 | (or (= "select" tag) 351 | (= "textarea" tag))) :nop 352 | (.startsWith attr "aria-") (render-attr-str! sb attr value) 353 | (not value) :nop 354 | (true? value) (append! sb " " attr "=\"\"") 355 | (.startsWith attr "on") :nop 356 | (= "dangerouslySetInnerHTML" attr) :nop 357 | :else (render-attr-str! sb attr value)))) 358 | 359 | 360 | (defn render-attrs! [tag attrs sb] 361 | (reduce-kv (fn [_ k v] (render-attr! tag k v sb)) nil attrs)) 362 | 363 | 364 | ;;; render html 365 | 366 | 367 | (defprotocol HtmlRenderer 368 | (-render-html [this parent sb] 369 | "Turn a Clojure data type into a string of HTML with react ids.")) 370 | 371 | 372 | (defn render-inner-html! [attrs children sb] 373 | (when-let [inner-html (:dangerouslySetInnerHTML attrs)] 374 | (when-not (empty? children) 375 | (throw (Exception. "Invariant Violation: Can only set one of `children` or `props.dangerouslySetInnerHTML`."))) 376 | (when-not (:__html inner-html) 377 | (throw (Exception. "Invariant Violation: `props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information."))) 378 | (append! sb (:__html inner-html)) 379 | true)) 380 | 381 | 382 | (defn render-textarea-value! [tag attrs sb] 383 | (when (= tag "textarea") 384 | (when-some [value (get-value attrs)] 385 | (append! sb (escape-html value)) 386 | true))) 387 | 388 | 389 | (defn render-content! [tag attrs children sb] 390 | (if (and (nil? children) 391 | (contains? void-tags tag)) 392 | (append! sb "/>") 393 | (do 394 | (append! sb ">") 395 | (or (render-textarea-value! tag attrs sb) 396 | (render-inner-html! attrs children sb) 397 | (doseq [element children] 398 | (-render-html element children sb))) 399 | (append! sb "")))) 400 | 401 | 402 | (defn render-element! 403 | "Render an element vector as a HTML element." 404 | [element sb] 405 | (when-not (nothing? element) 406 | (let [[tag id classes attrs children] (normalize-element element)] 407 | (append! sb "<" tag) 408 | 409 | (when-some [type (:type attrs)] 410 | (append! sb " type=\"" type "\"")) 411 | 412 | (when (and (= "option" tag) 413 | (= (get-value attrs) *select-value*)) 414 | (append! sb " selected=\"\"")) 415 | 416 | (when id 417 | (append! sb " id=\"" id "\"")) 418 | 419 | (render-attrs! tag attrs sb) 420 | 421 | (render-classes! classes sb) 422 | 423 | (if (= "select" tag) 424 | (binding [*select-value* (get-value attrs)] 425 | (render-content! tag attrs children sb)) 426 | (render-content! tag attrs children sb))))) 427 | 428 | 429 | (extend-protocol HtmlRenderer 430 | IPersistentVector 431 | (-render-html [this parent sb] 432 | (render-element! this sb)) 433 | 434 | ISeq 435 | (-render-html [this parent sb] 436 | (doseq [element this] 437 | (-render-html element parent sb))) 438 | 439 | Named 440 | (-render-html [this parent sb] 441 | (append! sb (name this))) 442 | 443 | String 444 | (-render-html [this parent sb] 445 | (append! sb (escape-html this))) 446 | 447 | Object 448 | (-render-html [this parent sb] 449 | (-render-html (str this) parent sb)) 450 | 451 | nil 452 | (-render-html [this parent sb] 453 | :nop)) 454 | 455 | 456 | (defn render-static-markup [src] 457 | (let [sb (StringBuilder.)] 458 | (-render-html src nil sb) 459 | (str sb))) 460 | 461 | (def render-html render-static-markup) 462 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _Prum (Pale Rum) is a fork of Rum library that uses Preact.js as an underlying UI rendering facility_ 2 | 3 | `[org.roman01la/prum "0.10.8-11"]` 4 | 5 | ### Differences to Rum/React 6 | 7 | #### No Hiccup interpretation 8 | 9 | Similar to Rum, Prum is using Hiccup compiler to transform Hiccup into JavaScript calls to Preact's API. However Hiccup interpretation is disabled in Prum, since it adds performance overhead. 10 | 11 | Due to this restrictions are the following: 12 | 13 | - Do not generate Hiccup elements programmatically 14 | - Wrap Hiccup elements with `prum.compiler/html` macro when returning Hiccup from a function 15 | - A list of forms that may contain Hiccup and will be handled by the compiler: [see here](https://github.com/rauhs/hicada/blob/master/src/hicada/compiler.clj#L111-L184) 16 | - If the first argument after the tag is a variable, it’s assumed to be the first child 17 | 18 | #### Creating ReactNodes from React class components 19 | 20 | ```clojure 21 | [:> ReactSelect {:value "value" :options options}] 22 | 23 | ;; (preact/createElement ReactSelect #js {:value value :options options}) 24 | ``` 25 | 26 | #### No string refs 27 | 28 | Preact supports only function _refs_. However string refs is still useful and easier to use in ClojureScript. To handle this properly there's a new helper function `rum.core/use-ref` 29 | 30 | ```clojure 31 | (rum/defc input [] 32 | [:input {}]) 33 | 34 | (rum/defcc form < 35 | {:after-render 36 | (fn [state] 37 | (rum/ref state :btn) ;; returns DOM node of the element 38 | (rum/ref state :input) ;; returns component 39 | (rum/ref-node state :input) ;; returns top-level DOM node of the component 40 | state)} 41 | 42 | [comp] 43 | 44 | [:form {} 45 | (rum/with-ref (input) (rum/use-ref comp :input)) 46 | [:button {:ref (rum/use-ref comp :btn)} "text"]]) 47 | ``` 48 | 49 | #### Context API 50 | 51 | Preact components doesn't implement `contextTypes` and `childContextTypes` as in React. This means that in Prum there's no need to declare `:contextTypes` and `:childContextTypes` in `:class-properties` mixin. 52 | 53 | Also there's a helper function to read from context `rum.core/context`. 54 | 55 | ```clojure 56 | (rum/defcc rum-context-comp [comp] 57 | [:span 58 | {:style {:color (rum/context comp :color)}} 59 | "Child component uses context to set font color."]) 60 | 61 | (rum/defc context < 62 | {:child-context (fn [state] {:color @core/*color})} 63 | [] 64 | [:div {} 65 | [:div {} "Root component implicitly passes data to descendants."] 66 | (rum-context-comp)]) 67 | ``` 68 | 69 | #### Re-rendering 70 | 71 | When re-rendering from the root, by default Preact appends to root DOM node. To re-render properly `rum.core/mount` accepts optional third argument, which is the root node to replace. 72 | 73 | ```clojure 74 | (def root (rum/mount (app) dom-node)) ;; returns root DOM node 75 | 76 | (rum/mount (app) dom-node root) ;; pass in the root node to render and replace 77 | ``` 78 | 79 | #### Collections of child elements 80 | 81 | When rendering a list of values, a collection of elements _should not be a vector_. 82 | 83 | ```clojure 84 | [:ul {} (mapv render-item items)] ;; this is wrong 85 | [:ul {} (map render-item items)] ;; this is ok 86 | 87 | [:ul {} [[:li {} "#1"] [:li {} "#2"]]] ;; this is wrong 88 | [:ul {} '([:li {} "#1"] [:li {} "#2"])] ;; this is ok 89 | ``` 90 | 91 | #### Dynamic CSS via `:css` attribute 92 | 93 | Provided by [Clojure Style Sheets](https://github.com/roman01la/cljss) library. 94 | 95 | ```clojure 96 | [:button {:css {:font-size "12px"}}] 97 | ``` 98 | 99 | #### DOM Events 100 | 101 | Preact use native (in-browser) event system instead of Synthetic Events system as in React, thus it doesn't change behaviour of DOM events. However to stay compatible with Rum/React, Prum translates `:on-change` handlers into `:on-input` as React does. 102 | 103 | _Below is original unmodified documentation of Rum_ 104 | 105 |

106 | 107 | Rum is a client/server library for HTML UI. In ClojureScript, it works as React wrapper, in Clojure, it is a static HTML generator. 108 | 109 | ### Principles 110 | 111 | **Simple semantics**: Rum is arguably smaller, simpler and more straightforward than React itself. 112 | 113 | **Decomplected**: Rum is a library, not a framework. Use only the parts you need, throw away or replace what you don’t need, combine different approaches in a single app, or even combine Rum with other frameworks. 114 | 115 | **No enforced state model**: Unlike Om, Reagent or Quiescent, Rum does not dictate where to keep your state. Instead, it works well with any storage: persistent data structures, atoms, DataScript, JavaScript objects, localStorage or any custom solution you can think of. 116 | 117 | **Extensible**: the API is stable and explicitly defined, including the API between Rum internals. It lets you build custom behaviours that change components in significant ways. 118 | 119 | **Minimal codebase**: You can become a Rum expert just by reading its source code (~900 lines). 120 | 121 | ### Comparison to other frameworks 122 | 123 | Rum: 124 | 125 | - does not dictate how to store your state, 126 | - has server-side rendering, 127 | - is much smaller. 128 | 129 | ### Who’s using Rum? 130 | 131 | - [Cognician](https://www.cognician.com), coaching platform 132 | - [Attendify](https://attendify.com), mobile app builder 133 | - [PartsBox.io](https://partsbox.io), inventory management 134 | - [modnaKasta](https://modnaKasta.ua), online shopping 135 | - [ChildrensHeartSurgery.info](http://childrensheartsurgery.info), heart surgery statistics 136 | - [Mighty Hype](http://mightyhype.com/), cinema platform (server-side rendering) 137 | - [БезопасныеДороги.рф](https://xn--80abhddbmm5bieahtk5n.xn--p1ai/), road data aggregator 138 | - [TourneyBot](http://houstonindoor.com/2016), frisbee tournament app 139 | - [PurposeFly](https://www.purposefly.com/), HR 2.0 platform 140 | 141 | ## Using Rum 142 | 143 | Add to project.clj: `[rum "0.10.8"]` 144 | 145 | ### Defining a component 146 | 147 | Use `rum.core/defc` (short for “define component”) to define a function that returns component markup: 148 | 149 | ```clojure 150 | (require [rum.core :as rum]) 151 | 152 | (rum/defc label [text] 153 | [:div {:class "label"} text]) 154 | ``` 155 | 156 | Rum uses Hiccup-like syntax for defining markup: 157 | 158 | ```clojure 159 | [ ? *] 160 | ``` 161 | 162 | `` defines a tag, its id and classes: 163 | 164 | ```clojure 165 | :span 166 | :span#id 167 | :span.class 168 | :span#id.class 169 | :span.class.class2 170 | ``` 171 | 172 | By default, if you omit the tag, `div` is assumed: 173 | 174 | ``` 175 | :#id === :div#id 176 | :.class === :div.class 177 | ``` 178 | 179 | `` is an optional map of attributes: 180 | 181 | - Use kebab-case keywords for attributes (e.g. `:allow-full-screen` for `allowFullScreen`) 182 | - You can include `:id` and `:class` there as well 183 | - `:class` can be a string or a sequence of strings 184 | - `:style`, if needed, must be a map with kebab-case keywords 185 | - event handlers should be arity-one functions 186 | 187 | ```clojure 188 | [:input { :type "text" 189 | :allow-full-screen true 190 | :id "comment" 191 | :class ["input_active" "input_error"] 192 | :style { :background-color "#EEE" 193 | :margin-left 42 } 194 | :on-change (fn [e] 195 | (js/alert (.. e -target -value))) }] 196 | ``` 197 | 198 | `` is a zero, one or many elements (strings or nested tags) with the same syntax: 199 | 200 | ```clojure 201 | [:div {} "Text"] ;; tag, attrs, nested text 202 | [:div {} [:span]] ;; tag, attrs, nested tag 203 | [:div "Text"] ;; omitted attrs 204 | [:div "A" [:em "B"] "C"] ;; 3 children, mix of text and tags 205 | ``` 206 | 207 | Children can include lists or sequences which will be flattened: 208 | 209 | ```clojure 210 | [:div (list [:i "A"] [:b "B"])] === [:div [:i "A"] [:b "B"]] 211 | ``` 212 | 213 | By default all text nodes are escaped. To embed an unescaped string into a tag, add the `:dangerouslySetInnerHTML` attribute and omit children: 214 | 215 | ```clojure 216 | [:div { :dangerouslySetInnerHTML {:__html ""}}] 217 | ``` 218 | 219 | ### Rendering component 220 | 221 | Given this code: 222 | 223 | ```clojure 224 | (require [rum.core :as rum]) 225 | 226 | (rum/defc repeat-label [n text] 227 | [:div (repeat n [:.label text])]) 228 | ``` 229 | 230 | First, we need to create a component instance by calling its function: 231 | 232 | ``` 233 | (repeat-label 5 "abc") 234 | ``` 235 | 236 | Then we need to pass that instance to `(rum.core/mount comp dom-node)`: 237 | 238 | ```clojure 239 | (rum/mount (repeat-label 5 "abc") js/document.body) 240 | ``` 241 | 242 | And we will get this result: 243 | 244 | ```html 245 | 246 |
247 |
abc
248 |
abc
249 |
abc
250 |
abc
251 |
abc
252 |
253 | 254 | ``` 255 | 256 | Usually, `mount` is used just once in an app lifecycle to mount the top of your component tree to a page. After that, for a dynamic applications, you should either _update_ your components or rely on them to update themselves. 257 | 258 | ### Updating components manually 259 | 260 | The simplest way to update your app is to mount it again: 261 | 262 | ```clojure 263 | (rum/defc timer [] 264 | [:div (.toISOString (js/Date.))]) 265 | 266 | (rum/mount (timer) js/document.body) 267 | 268 | (js/setInterval 269 | #(rum/mount (timer) js/document.body) 270 | 1000) 271 | ``` 272 | 273 | ### Reactive components 274 | 275 | Rum offers mixins as a way to hook into a component’s lifecycle and extend its capabilities or change its behaviour. 276 | 277 | One very common use-case is for a component to update when some reference changes. Rum has a `rum.core/reactive` mixin just for that: 278 | 279 | ```clojure 280 | (def count (atom 0)) 281 | 282 | (rum/defc counter < rum/reactive [] 283 | [:div { :on-click (fn [_] (swap! count inc)) } 284 | "Clicks: " (rum/react count)]) 285 | 286 | (rum/mount (counter) js/document.body) 287 | ``` 288 | 289 | Two things are happening here: 290 | 291 | 1. We’re adding the `rum.core/reactive` mixin to the component. 292 | 2. We’re using `rum.core/react` instead of `deref` in the component body. 293 | 294 | This will set up a watch on the `count` atom and will automatically call `rum.core/request-render` on the component each time the atom changes. 295 | 296 | 297 | ### Component’s local state 298 | 299 | Sometimes you need to keep track of some mutable data just inside a component and nowhere else. Rum provides the `rum.core/local` mixin. It’s a little trickier to use, so hold on: 300 | 301 | 1. Each component in Rum has internal state associated with it, normally used by mixins and Rum internals. 302 | 2. `rum.core/local` creates a mixin that will put an atom into the component’s state. 303 | 3. `rum.core/defcs` is used instead of `rum.core/defc`. It allows you to get hold of the components’s state in the render function (it will be passed as a first argument). 304 | 4. You can then extract that atom from the component’s state and `deref`/`swap!`/`reset!` it as usual. 305 | 5. Any change to the atom will force the component to update. 306 | 307 | In practice, it’s quite convenient to use: 308 | 309 | ```clojure 310 | (rum/defcs stateful < (rum/local 0 ::key) 311 | [state label] 312 | (let [local-atom (::key state)] 313 | [:div { :on-click (fn [_] (swap! local-atom inc)) } 314 | label ": " @local-atom])) 315 | 316 | (rum/mount (stateful "Click count") js/document.body) 317 | ``` 318 | 319 | ### Optimizing with shouldComponentUpdate 320 | 321 | If your component accepts only immutable data structures as arguments, it may be a good idea to add the `rum.core/static` mixin: 322 | 323 | ```clojure 324 | (rum/defc label < rum/static [n text] 325 | [:.label (repeat n text)]) 326 | ``` 327 | 328 | `rum.core/static` will check if the arguments of a component’s constructor have changed (using Clojure’s `-equiv` semantic), and if they are the same, avoid re-rendering. 329 | 330 | ```clojure 331 | (rum/mount (label 1 "abc") body) 332 | (rum/mount (label 1 "abc") body) ;; render won’t be called 333 | (rum/mount (label 1 "xyz") body) ;; this will cause a re-render 334 | (rum/mount (label 1 "xyz") body) ;; this won’t 335 | ``` 336 | 337 | Note that this is not enabled by default because a) comparisons can be expensive, and b) things will go wrong if you pass a mutable reference as an argument. 338 | 339 | ### Writing your own mixin 340 | 341 | Many applications have very specific requirements and custom optimization opportunities, so odds are you’ll be writing your own mixins. 342 | 343 | Let’s see what a Rum component really is. Each Rum component has: 344 | 345 | - A render function 346 | - One or more mixins 347 | - An internal state map 348 | - A corresponding React component 349 | 350 | For example, if we have this component defined: 351 | 352 | ```clojure 353 | (rum/defc input [label value] 354 | [:label label ": " 355 | [:input { :value value }]]) 356 | 357 | (input "Your name" "") 358 | ``` 359 | 360 | It will have the following state: 361 | 362 | ```clojure 363 | { :rum/args ["Your name" ""] 364 | :rum/react-component } 365 | ``` 366 | 367 | You can read the internal state by using the `rum.core/defcs` (short for “define component [and pass] state”) macro instead of `rum.core/defc`. It will pass `state` to the render function as the first argument: 368 | 369 | ```clojure 370 | (rum/defcs label [state label value] 371 | [:div "My args:" (pr-str (:rum/args state))]) 372 | 373 | (label "A" 3) ;; =>
My args: ["A" 3]
374 | ``` 375 | 376 | The internal state cannot be directly manipulated, except at certain stages of a component’s lifecycle. Mixins are functions that are invoked at these stages to give you and opportunity to modify the state and/or do side effects to the world. 377 | 378 | The following mixin will record the component’s mount time: 379 | 380 | ```clojure 381 | (rum/defcs time-label < { :will-mount (fn [state] 382 | (assoc state ::time (js/Date.))) } 383 | [state label] 384 | [:div label ": " (str (::time state))]) 385 | ``` 386 | 387 | As you can see, `:will-mount` is a function from `state` to `state`. It gives you a chance to populate, clean or modify state map the moment before the component has been mounted. 388 | 389 | Another useful thing you can do in a mixin is to decide when to update a component. If you can get ahold of React component (notice that that’s different from Rum component, unfortunately; sorry), you can call `rum.core/request-render` to schedule this component’s update at next frame (Rum uses `requestAnimationFrame` to batch and debounce component update calls). To get React component, just look up `:rum/react-component` key in a state. 390 | 391 | This mixin will update a component each second: 392 | 393 | ```clojure 394 | (def periodic-update-mixin 395 | { :did-mount (fn [state] 396 | (let [comp (:rum/react-component state) 397 | callback #(rum/request-render comp) 398 | interval (js/setInterval callback 1000)] 399 | (assoc state ::interval interval))) 400 | :will-unmount (fn [state] 401 | (js/clearInterval (::interval state)) 402 | (dissoc state ::interval)) }) 403 | 404 | (rum/defc timer < periodic-update-mixin [] 405 | [:div (.toISOString (js/Date.))]) 406 | 407 | (rum/mount (timer) js/document.body) 408 | ``` 409 | 410 | Here’s a full list of callbacks you can define in a mixin: 411 | 412 | ```clojure 413 | { :init ;; state, props ⇒ state 414 | :will-mount ;; state ⇒ state 415 | :before-render ;; state ⇒ state 416 | :wrap-render ;; render-fn ⇒ render-fn 417 | :render ;; state ⇒ [pseudo-dom state] 418 | :did-mount ;; state ⇒ state 419 | :after-render ;; state ⇒ state 420 | :did-remount ;; old-state, state ⇒ state 421 | :should-update ;; old-state, state ⇒ boolean 422 | :will-update ;; state ⇒ state 423 | :did-update ;; state ⇒ state 424 | :will-unmount } ;; state ⇒ state 425 | ``` 426 | 427 | Each component can have any number of mixins: 428 | 429 | ```clojure 430 | (rum/defcs component < rum/static 431 | rum/reactive 432 | (rum/local 0 ::count) 433 | (rum/local "" ::text) 434 | [state label] 435 | (let [count-atom (::count state) 436 | text-atom (::text state)] 437 | [:div]) 438 | ``` 439 | 440 | One gotcha: don’t forget to return `state` from the mixin functions. If you’re using them for side-effects only, just return an unmodified `state`. 441 | 442 | 443 | ### Working with atoms 444 | 445 | Since Rum relies a lot at components being able to efficiently update themselves in reaction to events, it includes two facilities to build architectures around Atoms and watchers. 446 | 447 | **Cursors** 448 | 449 | If you have a complex state and need a component to interact with only a part of it, create a cursor using `(rum.core/cursor-in ref path)`. Given atom with deep nested value and path inside it, `cursor-in` will create an atom-like structure that can be used separately from main atom, but will sync changes both ways: 450 | 451 | ```clojure 452 | (def state (atom { :color "#cc3333" 453 | :user { :name "Ivan" } })) 454 | 455 | (def user-name (rum/cursor-in state [:user :name])) 456 | 457 | @user-name ;; => "Ivan" 458 | 459 | (reset! user-name "Oleg") ;; => "Oleg" 460 | 461 | @state ;; => { :color "#cc3333" 462 | ;; :user { :name "Oleg" } } 463 | ``` 464 | 465 | Cursors implement `IAtom` and `IWatchable` and interface-wise are drop-in replacement for regular atoms. They work well with `rum/reactive` and `rum/react` too. 466 | 467 | **Derived atoms** 468 | 469 | Use derived atoms to create “chains” and acyclic graphs of dependent atoms. `derived-atom` will: 470 | 471 | - Take N “source” refs 472 | - Set up a watch on each of them 473 | - Create “sink” atom 474 | - When any of source refs changes: 475 | - re-run function `f`, passing N dereferenced values of source refs 476 | - `reset!` result of `f` to the sink atom 477 | - return sink atom 478 | 479 | ```clojure 480 | (def *a (atom 0)) 481 | (def *b (atom 1)) 482 | (def *x (derived-atom [*a *b] ::key 483 | (fn [a b] 484 | (str a \":\" b)))) 485 | (type *x) ;; => clojure.lang.Atom 486 | @*x ;; => 0:1 487 | (swap! *a inc) 488 | @*x ;; => 1:1 489 | (reset! *b 7) 490 | @*x ;; => 1:7 491 | ``` 492 | 493 | Derived atoms are like cursors, but can “depend on” multiple references and won’t sync changes back to the source if you try to update derived atom (don’t). 494 | 495 | 496 | ### Interop with React 497 | 498 | **Native React component** 499 | 500 | You can access the raw React component by reading the state’s `:rum/react-component` attribute: 501 | 502 | ```clojure 503 | { :did-mount (fn [state] 504 | (let [comp (:rum/react-component state) 505 | dom-node (js/ReactDOM.findDOMNode comp)] 506 | (set! (.-width (.-style dom-node)) "100px")) 507 | state) } 508 | ``` 509 | 510 | **React keys and refs** 511 | 512 | There’re three ways to specify React keys: 513 | 514 | 1. If you need a key on Sablono tag, put it into attributes: `[:div { :key "x" }]` 515 | 2. If you need a key on Rum component, use `with-key`: 516 | 517 | ```clojure 518 | (rum/defc my-component [str] 519 | ...) 520 | 521 | (rum/with-key (my-component "args") "x") 522 | ``` 523 | 3. or, you can specify `:key-fn` in a mixin to calculate key based on args at component creation time: 524 | 525 | ```clojure 526 | (rum/defc my-component < { :key-fn (fn [x y z] 527 | (str x "-" y "-" z)) } 528 | [x y z] 529 | ...) 530 | 531 | (my-component 1 2 3) ;; => key == "1-2-3" 532 | ``` 533 | 534 | `:key-fn` must accept same arguments your render function does. 535 | 536 | Refs work the same way as options 1 and 2 for keys work: 537 | 538 | 1. `[:div { :ref "x" }]` 539 | 2. `(rum/with-ref (my-component) "x")` 540 | 541 | 542 | **Accessing DOM** 543 | 544 | There’re couple of helpers that will, given state map, find stuff in it for you: 545 | 546 | ```clojure 547 | (rum/dom-node state) ;; => top-level DOM node 548 | (rum/ref state "x") ;; => ref-ed React component 549 | (rum/ref-node state "x") ;; => top-level DOM node of ref-ed React component 550 | ``` 551 | 552 | **Custom class properties** 553 | 554 | To define arbitrary properties and methods on a component class, specify a `:class-properties` map in a mixin: 555 | 556 | ```clojure 557 | (rum/defc comp < { :class-properties { ... } } 558 | [:div])) 559 | ``` 560 | 561 | **React context** 562 | 563 | To define child context, specify a `:child-context` function taking state and returning context map in a mixin: 564 | 565 | ```clojure 566 | (rum/defc theme < { :child-context 567 | (fn [state] 568 | (let [[color] (:rum/args state)] 569 | { :color color })) 570 | :class-properties 571 | { :childContextTypes {:color js/React.PropTypes.string} } } 572 | [color child] 573 | child) 574 | ``` 575 | 576 | ### Server-side rendering 577 | 578 | If used from clj/cljc, Rum works as a traditional template engine à la Hiccup: 579 | 580 | 1. Import `rum.core` as usual. 581 | 2. Define components using `rum/defc` or other macros as usual. 582 | 3. Instead of mounting, call `rum/render-html` to render into a string. 583 | 4. Generate the HTML page using that string. 584 | 5. On the client side, mount _the same_ component over the node where you rendered your server-side component. 585 | 586 | ```clojure 587 | (require '[rum.core :as rum]) 588 | 589 | (rum/defc my-comp [s] 590 | [:div s]) 591 | 592 | ;; on a server 593 | (rum/render-html (my-comp "hello")) 594 | ;; => "
hello
" 595 | 596 | ;; on a client 597 | (rum/mount (my-comp "hello") js/document.body) 598 | ``` 599 | 600 | Use `rum/render-static-markup` if you’re not planning to connect your page with React later: 601 | 602 | ```clojure 603 | (rum/render-static-markup (my-comp "hello")) ;; =>
hello
604 | ``` 605 | 606 | Rum server-side rendering does not use React or Sablono, it runs completely in JVM, without involving JavaScript at any stage. 607 | 608 | As of `[rum "0.8.3"]` and `[hiccup "1.0.5"]`, Rum is ~3× times faster than Hiccup. 609 | 610 | Server-side components do not have full lifecycle support, but `:init` and `:will-mount` from mixins would be called at the component’s construction time. 611 | 612 | ## Resources 613 | 614 | - Ask for help on [Gitter chat](https://gitter.im/tonsky/rum) 615 | - Check out [our wiki](https://github.com/tonsky/rum/wiki) 616 | 617 | ### Talks 618 | 619 | - [Rum workshop](https://www.youtube.com/watch?v=RqHnxkU9TZE) at Cognician, by me 620 | - [Norbert Wójtowicz talk at Lambda Days 2015](https://vimeo.com/122316380) where he explains benefits of web development with ClojureScript and React, and how Rum emulates all main ClojureScript frameworks 621 | - [Hangout about Rum](https://www.youtube.com/watch?v=8evDKjD5vt4) (in Russian) 622 | 623 | ### App templates 624 | 625 | - [Tenzing](https://github.com/martinklepsch/tenzing) 626 | 627 | ### Libraries 628 | 629 | - [Reforms](http://bilus.github.io/reforms/), Bootstrap 3 forms 630 | - [rum-mdl](http://ajchemist.github.io/rum-mdl/), Material design lite components 631 | - [derivatives](https://github.com/martinklepsch/derivatives), creates chains of derived values from an atom 632 | - [scrum](https://github.com/roman01la/scrum), state coordination framework 633 | - [Antizer](https://github.com/priornix/antizer) Ant Design component library 634 | 635 | ### Examples 636 | 637 | - In this repo see [examples/rum/examples/](examples/prum/examples/). [Live version](http://tonsky.me/rum/) 638 | - [DataScript Chat app](https://github.com/tonsky/datascript-chat) 639 | - [DataScript ToDo app](https://github.com/tonsky/datascript-todo) 640 | - [DataScript Menu app](https://github.com/tonsky/datascript-menu) 641 | 642 | ## Changes 643 | 644 | ### 0.10.8 645 | 646 | - React 15.4.2-0, Sablono 0.7.7 647 | - Render boolean `aria-*` values as strings (thx [r0man](https://github.com/r0man), PR #114) 648 | - Escape attributes during server-side rendering (thx [Alexander Solovyov](https://github.com/piranha), PR #115) 649 | 650 | ### 0.10.7 651 | 652 | - Fixed server-side rendering discrepancy (issue #99) 653 | - Sablono 0.7.5, React 15.3.1-0 654 | 655 | ### 0.10.6 656 | 657 | - Sablono 0.7.4 [fixes the issue](https://github.com/r0man/sablono/pull/129) with controlling components refusing to change value if non-string value was used 658 | - React 15.3.0-0 659 | - Throw error when `<` is misplaced in `defc` (thx [Martin Klepsch](https://github.com/martinklepsch), issue #88, PR #90) 660 | 661 | ### 0.10.5 662 | 663 | - Sablono 0.7.3 fixes the issue when IE lost keystrokes in controlled inputs/textarea (#86) 664 | - React 15.2.1-1 665 | - Warn when `rum.core/react` is used without `rum.core/reactive` (thx [Martin Klepsch](https://github.com/martinklepsch), issue #82, PR #87) 666 | 667 | ### 0.10.4 668 | 669 | - Ability to use `:pre` and `:post` checks in `rum.core/defc` (thx [Martin Klepsch](https://github.com/martinklepsch), PR #81) 670 | 671 | ### 0.10.3 672 | 673 | - Fixed regression of `displayName` in 0.10.0 674 | - Bumped React to 15.2.0 675 | 676 | ### 0.10.2 677 | 678 | - Fixed a bug when `:before-render` and `:will-update` weren’t called on subsequent renders 679 | 680 | ### 0.10.1 681 | 682 | - Made `rum.core/state` public again 683 | - `:before-render` should be called on server-side rendering too (thx [Alexander Solovyov](https://github.com/piranha), PR #79) 684 | 685 | ### 0.10.0 686 | 687 | A big cleanup/optmization/goodies release with a lot breaking changes. Read carefully! 688 | 689 | - [ BREAKING ] `cursor` got renamed to `cursor-in`. New `cursor` method added that takes single key (as everywhere in Clojure) 690 | - [ BREAKING ] `rum/mount` returns `nil` (because you [shouldn’t rely on return value of ReactDOM.render](https://github.com/facebook/react/issues/4936)) 691 | - [ BREAKING ] `:transfer-state` is gone. All of component’s state is now transferred by default. If you still need to do something fancy on `componentWillReceiveProps`, new callback is called `:did-remount` callback 692 | - [ BREAKING ] removed `cursored` and `cursored-watch` mixins. They felt too unnatural to use 693 | - [ BREAKING ] removed `rum/with-props` (deprecated since 0.3.0). Use `rum/with-key` and `rum/with-ref` instead 694 | - [ BREAKING ] server-side rendering no longer calls `:did-mount` (obviously, that was a mistake) 695 | - [ BREAKING ] `:rum/id` is gone. If you need an unique id per component, allocate one in `:init` as store it in state under namespaced key 696 | 697 | When upgrading to 0.10.0, check this migration checklist: 698 | 699 | - Change all `rum/cursor` calls to `rum/cursor-in` 700 | - Find all `:transfer-state` mixins. 701 | - If the only thing they were doing is something like `(fn [old new] (assoc new ::key (::key old)))`, just delete them. 702 | - If not, rename to `:did-remount` 703 | - Check if you were using `rum/mount` return value. If yes, find another way to obtain component (e.g. via `ref`, `defcc` etc) 704 | - Replace `rum/with-props` with `rum/with-key`, `rum/with-ref` or `:key-fn` 705 | - Check that you weren’t relying on `:did-mount` in server-side rendering 706 | 707 | Now for the good stuff: 708 | 709 | - Cursors now support metadata, `alter-meta!` etc 710 | - Cursors can be used from Clojure 711 | - Added `:key-fn` to mixins. That function will be called before element creation, with same arguments as render fn, and its return value will be used as a key on that element 712 | - Mixins can specify `:before-render` (triggered at `componentWillMount` and `componentWillUpdate`) and `:after-render` (`componentDidMount` and `componentDidUpdate`) callback 713 | - Added `rum/ref` and `rum/ref-node` helpers, returning backing component and DOM node 714 | - Some client-side API functions added to server version (`dom-node`, `unmount`, `request-render` etc). Their implementation just throws an exception. This is to help you write less conditional directives in e.g. `:did-mount` or `:will-unmount` mixins. They will never be called, but won’t stop code from compiling either. 715 | 716 | And couple of optimizations: 717 | 718 | - Rum now makes use of staless components (nothing for you to do, if your component is defined via `defc` with no mixins, it’ll be automatically compiled to stateless component) 719 | - Rum will use React’s batched updates to perform rendering on `requestAnimationFrame` in a single chunk 720 | - Streamlined internals of component construction, removed `render->mixin`, `args->state`, `element` and `ctor->class` 721 | 722 | 723 | ### 0.9.1 724 | 725 | - Added `rum.core/derived-atom`, a function that let you build reactive chains and directed acyclic graphs of dependent atoms. E.g. you want `*c` to always contain a value of `*a` plus a value of `*b` and update whenever any of them changes 726 | - Added `rum.core/dom-node` helper that takes state and finds corresponding top DOM node of a component. Can be called in mixins after initial render only 727 | - Fixed compatibility of `with-key` on nil-returning component in server rendering (thx [Alexander Solovyov](https://github.com/piranha), PR #73) 728 | 729 | ### 0.9.0 730 | 731 | - Better support for server-side rendering of SVG 732 | - [ BREAKING ] Rum used to support multiple ways to specify attributes. You would expect that both `:allow-full-screen`, `:allowFullScreen` and `"allowFullScreen"` would be normalized to `allowfullscreen`. As a result, you have to face three problems: 733 | - how do I decide which variant to use? 734 | - how do I ensure consistency accross my team and our codebase? 735 | - find & replace become harder 736 | 737 | Starting with 0.9.0, Rum will adopt “There’s Only One Way To Do It” policy. All attributes MUST be specified as kebab-cased keywords: 738 | 739 | | Attribute | What to use | What not to use | 740 | | --------- | ----------- | --------------- | 741 | | class | `:class` | ~~`:class-name`~~ ~~`:className`~~ | 742 | | for | `:for` | ~~`:html-for`~~ ~~`:htmlFor`~~ | 743 | | unescaped innerHTML | `:dangerouslySetInnerHTML { :__html { "..." }}` | | 744 | | uncontrolled value | `:default-value` | ~~`:defaultValue`~~ | 745 | | uncontrolled checked | `:default-checked` | ~~`:defaultChecked`~~ | 746 | | itemid, classid | `:item-id`, `:class-id` | ~~`:itemID`~~ ~~`:itemId`~~ ~~`:itemid`~~| 747 | | xml:lang etc | `:xml-lang` | ~~`:xml/lang`~~ ~~`:xmlLang`~~ ~~`"xml:lang"`~~ | 748 | | xlink:href etc | `:xlink-href` | ~~`:xlink/href`~~ ~~`:xlinkHref`~~ ~~`"xlink:href"`~~ | 749 | | xmlns | not supported | | 750 | 751 | To migrate to 0.9.0 from earlier versions, just do search-and-replace for non-standard variants and replace them with recommended ones. 752 | 753 | ### 0.8.4 754 | 755 | - Improved server-side rendering for inputs (issue #67 & beyond) 756 | - Compatible server-side rendering of components that return nil (issue #64) 757 | - Upgraded React to 15.1.0 758 | 759 | ### 0.8.3 760 | 761 | - `rum/render-static-markup` call for pure HTML templating. Use it if you’re not planning to connect your page with React later 762 | - `rum/def*` macros now correctly retain metadata that already exists on a symbol (thx [aJchemist](https://github.com/aJchemist), PR #62) 763 | 764 | ### 0.8.2 765 | 766 | - Add `rum.core/unmount` function (thx [emnh](https://github.com/emnh), issue #61) 767 | 768 | ### 0.8.1 769 | 770 | - Retain `:arglists` metadata on vars defined by `rum/def*` macros (thx [aJchemist](https://github.com/aJchemist), PR #60) 771 | 772 | ### 0.8.0 773 | 774 | - Migrated to React 15.0.1 775 | - Optimized server-side rendering (~4× faster than Rum 0.7.0, ~2-3× faster than Hiccup 1.0.5) 776 | 777 | ### 0.7.0 778 | 779 | - Server-side rendering via `rum/render-html` (thx [Alexander Solovyov](https://github.com/piranha)) 780 | 781 | ### 0.6.0 782 | 783 | - [ BREAKING ] Updated to [React 0.14.3](https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html) (thx [Andrey Antukh](https://github.com/niwinz), PR #53) 784 | 785 | ### 0.5.0 786 | 787 | - Added `:class-properties` to define arbitrary properties on a React class (thx [Karanbir Toor](https://github.com/currentoor), PR #44) 788 | - [ BREAKING ] Removed support for `:child-context-types` and `:context-types`. Use `{ :class-properties { :childContextTypes ..., :contextTypes ... } }` instead. 789 | 790 | ### 0.4.2 791 | 792 | - Check for `setTimeout` in global scope instead of in window (thx [Alexander Solovyov](https://github.com/piranha), PR #43) 793 | 794 | ### 0.4.1 795 | 796 | - Fixed bug with rum macros emitting wrong namespace. You can now require `rum.core` under any alias you want (thx [Stuart Hinson](https://github.com/stuarth), PR #42) 797 | 798 | ### 0.4.0 799 | 800 | - [ BREAKING ] Core namespace was renamed from `rum` to `rum.core` to supress CLJS warnings 801 | 802 | ### 0.3.0 803 | 804 | - Upgraded to React 0.13.3, Sablono 0.3.6, ClojueScript 1.7.48 805 | - New API to access context: `child-context`, `child-context-types`, `context-types` (thx [Karanbir Toor](https://github.com/currentoor), PR #37) 806 | - New `defcc` macro for when you only need React component, not the whole Rum state 807 | - [ BREAKING ] Component inner state (`:rum/state`) was moved from `props` to `state`. It doesn’t change a thing if you were using Rum API only, but might break something if you were relaying on internal details 808 | - Deprecated `rum/with-props` macro, use `rum/with-key` or `rum/with-ref` fns instead 809 | 810 | ### 0.2.7 811 | 812 | - Allow components to refer to themselves (thx [Kevin Lynagh](https://github.com/lynaghk), pull request #30) 813 | - Support for multi-arity render fns (issue #23) 814 | 815 | ### 0.2.6 816 | 817 | - Added `local` mixin 818 | 819 | ### 0.2.5 820 | 821 | - Fixed argument destructuring in defc macro (issue #22) 822 | 823 | ### 0.2.4 824 | 825 | - `will-update` and `did-update` lifecycle methods added (thx [Andrey Vasenin](https://github.com/avasenin), pull request #18) 826 | 827 | ### 0.2.3 828 | 829 | - Components defined via `defc/defcs` will have `displayName` defined (thx [Ivan Dubrov](https://github.com/idubrov), pull request #16) 830 | - Not referencing `requestAnimationFrame` when used in headless environment (thx @[whodidthis](https://github.com/whodidthis), pull request #14) 831 | 832 | ### 0.2.2 833 | 834 | - Compatibility with clojurescript 0.0-2758, macros included automatically when `(:require rum)` 835 | 836 | ### 0.2.1 837 | 838 | - Updated deps to clojurescript 0.0-2727, react 0.12.2-5 and sablono 0.3.1 839 | 840 | ### 0.2.0 841 | 842 | - [ BREAKING ] New syntax for mixins: `(defc name < mixin1 mixin2 [args] body...)` 843 | - New `defcs` macro that adds additional first argument to render function: `state` 844 | - Ability to specify `key` and `ref` to rum components via `with-props` 845 | 846 | ### 0.1.1 847 | 848 | - Fixed a bug when render-loop tried to `.forceUpdate` unmounted elements 849 | - Fixed a cursor leak bug in `reactive` mixin 850 | - Removed `:should-update` from `reactive`, it now will be re-rendered if re-created by top-level element 851 | - Combine `reactive` with `static` to avoid re-rendering if component is being recreated with the same args 852 | 853 | ## Acknowledgements 854 | 855 | Rum was build on inspiration from [Quiescent](https://github.com/levand/quiescent), [Om](https://github.com/swannodette/om) and [Reagent](https://github.com/reagent-project/reagent). 856 | 857 | All heavy lifting done by [React](http://facebook.github.io/react/), [Ŝablono](https://github.com/r0man/sablono) and [ClojureScript](https://github.com/clojure/clojurescript). 858 | 859 | ## License 860 | 861 | Copyright © 2014–2016 Nikita Prokopov 862 | 863 | Licensed under Eclipse Public License (see [LICENSE](LICENSE)). 864 | --------------------------------------------------------------------------------