├── .github └── FUNDING.yml ├── scripts └── repl ├── .yarnrc.yml ├── doc ├── notes.txt └── Makefile ├── test └── user.clj ├── doc.clj ├── .gitignore ├── shadow-cljs.edn ├── examples ├── rumext │ └── examples │ │ ├── core.cljs │ │ ├── portals.cljs │ │ ├── errors.cljs │ │ ├── refs.cljs │ │ ├── timer_reactive.cljs │ │ ├── controls.cljs │ │ ├── board.cljs │ │ ├── util.cljs │ │ ├── local_state.cljs │ │ └── binary_clock.cljs └── public │ └── index.html ├── package.json ├── deps.edn ├── pom.xml ├── src └── rumext │ ├── v2 │ ├── validation.cljs │ ├── normalize.clj │ ├── util.cljc │ └── compiler.clj │ ├── v2.cljs │ └── v2.clj ├── yarn.lock ├── CHANGES.md ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: niwinz 2 | patreon: niwinz 3 | -------------------------------------------------------------------------------- /scripts/repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | clojure -A:dev -J-Xms128m -J-Xmx128m -M -m rebel-readline.main 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | enableImmutableCache: false 4 | 5 | enableImmutableInstalls: false 6 | 7 | enableTelemetry: false 8 | 9 | nodeLinker: node-modules 10 | -------------------------------------------------------------------------------- /doc/notes.txt: -------------------------------------------------------------------------------- 1 | Build browserified bundle: 2 | ./node_modules/browserify/bin/cmd.js -s Rx -e dist/cjs/Rx.js -o rx.js 3 | 4 | Minified bundle: 5 | ./node_modules/uglify-js/bin/uglifyjs rx.js -m -o rx.min.js 6 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | all: doc 2 | 3 | doc: 4 | mkdir -p dist/latest/ 5 | cd ..; clojure -A:dev:codox -M doc.clj; 6 | 7 | github: doc 8 | ghp-import -m "Generate documentation" -b gh-pages dist/ 9 | git push origin gh-pages 10 | -------------------------------------------------------------------------------- /test/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [clojure.tools.namespace.repl :as repl] 5 | [clojure.walk :refer [macroexpand-all]] 6 | [clojure.pprint :refer [pprint]] 7 | [clojure.repl :refer :all])) 8 | -------------------------------------------------------------------------------- /doc.clj: -------------------------------------------------------------------------------- 1 | (require '[codox.main :as codox]) 2 | 3 | (codox/generate-docs 4 | {:output-path "doc/dist/latest" 5 | :metadata {:doc/format :markdown} 6 | :language :clojurescript 7 | :name "funcool/rumext" 8 | :themes [:rdash] 9 | :source-paths ["src"] 10 | :namespaces [#"^rumext\."] 11 | :source-uri "https://github.com/funcool/rumext/blob/v2/{filepath}#L{line}"}) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !.yarn/patches 2 | !.yarn/plugins 3 | !.yarn/releases 4 | !.yarn/sdks 5 | !.yarn/versions 6 | *.class 7 | *.jar 8 | .pnp.* 9 | .shadow-cljs 10 | .yarn/* 11 | /*-init.clj 12 | /*-init.clj 13 | /.cpcache 14 | /.lein-* 15 | /.nrepl-port 16 | /.rebel_readline_history 17 | /.shadow-cljs 18 | /checkouts 19 | /classes 20 | /doc/dist 21 | /nashorn_code_cache 22 | /node_modules 23 | /out 24 | /repl 25 | /target 26 | pom.xml.asc -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps {:aliases [:dev]} 2 | :dev-http {9500 ["public" "classpath:public"]} 3 | 4 | :builds 5 | {:examples 6 | {:target :browser 7 | :output-dir "target/public/js" 8 | :asset-path "/js" 9 | :modules {:main {:entries [rumext.examples.core]}} 10 | :compiler-options {:output-feature-set :es-next} 11 | 12 | :js-options 13 | {:entry-keys ["module" "browser" "main"] 14 | :export-conditions ["module" "import", "browser" "require" "default"]} 15 | 16 | :release 17 | {:compiler-options 18 | {:pseudo-names false 19 | :pretty-print true}} 20 | 21 | }}} 22 | -------------------------------------------------------------------------------- /examples/rumext/examples/core.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.core 2 | (:require 3 | [rumext.examples.binary-clock :as binary-clock] 4 | [rumext.examples.timer-reactive :as timer-reactive] 5 | [rumext.examples.local-state :as local-state] 6 | [rumext.examples.refs :as refs] 7 | [rumext.examples.controls :as controls] 8 | [rumext.examples.portals :as portals] 9 | [rumext.examples.board :as board] 10 | ;; [rumext.examples.errors :as errors] 11 | )) 12 | 13 | (enable-console-print!) 14 | 15 | (local-state/mount!) 16 | (binary-clock/mount!) 17 | (timer-reactive/mount!) 18 | (refs/mount!) 19 | (controls/mount!) 20 | (board/mount!) 21 | (portals/mount!) 22 | 23 | (defn main 24 | [& args] 25 | (js/console.log "main" args)) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rumext", 3 | "version": "1.0.0", 4 | "description": "Simple and Decomplected UI library based on React.", 5 | "dependencies": { 6 | "process": "^0.11.10", 7 | "react": "19.1.0", 8 | "react-dom": "19.1.0" 9 | }, 10 | "scripts": { 11 | "watch": "clojure -M:dev:shadow-cljs watch examples" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/funcool/rumext.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/funcool/rumext/issues" 21 | }, 22 | "homepage": "https://github.com/funcool/rumext#readme", 23 | "packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538" 24 | } 25 | -------------------------------------------------------------------------------- /examples/rumext/examples/portals.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.portals 2 | (:require 3 | [rumext.v2 :as mf] 4 | [goog.dom :as dom])) 5 | 6 | (mf/defc portal* 7 | [{:keys [state]}] 8 | [:div {:on-click (fn [_] (swap! state inc)) 9 | :style { :user-select "none", :cursor "pointer" }} 10 | "[ PORTAL Clicks: " @state " ]"]) 11 | 12 | (mf/defc portals* 13 | [] 14 | (let [state (mf/use-state 0)] 15 | [:div {:on-click (fn [_] (swap! state inc)) 16 | :style { :user-select "none", :cursor "pointer" }} 17 | "[ ROOT Clicks: " @state " ]" 18 | (mf/portal 19 | (mf/html [:> portal* {:state state}]) 20 | (dom/getElement "portal-off-root"))])) 21 | 22 | (defonce root 23 | (mf/create-root (dom/getElement "portals"))) 24 | 25 | (defn ^:after-load mount! [] 26 | (mf/render! root (mf/element portals*))) 27 | -------------------------------------------------------------------------------- /examples/rumext/examples/errors.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.errors 2 | #_(:require [rumext.core :as mx])) 3 | 4 | ;; (mx/defc faulty-render 5 | ;; [msg] 6 | ;; (throw (ex-info msg {}))) 7 | 8 | 9 | ;; (mx/defc faulty-mount 10 | ;; {:did-mount 11 | ;; (fn [state] 12 | ;; (let [[msg] (::mx/args state)] 13 | ;; (throw (ex-info msg {}))))} 14 | ;; [msg] 15 | ;; "Some test you’ll never see") 16 | 17 | 18 | ;; (mx/defcs child-error 19 | ;; {:did-catch 20 | ;; (fn [state error info] 21 | ;; (assoc state ::error error))} 22 | ;; [{error ::error, c ::mx/react-component} comp msg] 23 | ;; (if (some? error) 24 | ;; [:span "CAUGHT: " (str error)] 25 | ;; [:span "No error: " (comp msg)])) 26 | 27 | ;; (mx/defc errors 28 | ;; [] 29 | ;; [:span 30 | ;; (child-error faulty-render "render error") 31 | ;; #_(child-error faulty-mount "mount error")]) 32 | 33 | ;; (defn mount! [el] 34 | ;; (mx/mount (errors) el)) 35 | -------------------------------------------------------------------------------- /examples/rumext/examples/refs.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.refs 2 | (:require 3 | [goog.dom :as dom] 4 | [rumext.v2 :as mf])) 5 | 6 | (mf/defc textarea 7 | [props] 8 | (let [ref (mf/use-var) 9 | state (mf/use-state 0)] 10 | (mf/use-layout-effect 11 | nil 12 | (fn [] 13 | (let [node @ref] 14 | (set! (.-height (.-style node)) "0") 15 | (set! (.-height (.-style node)) (str (+ 2 (.-scrollHeight node)) "px"))))) 16 | 17 | [:textarea 18 | {:ref ref 19 | :style {:width "100%" 20 | :padding "10px" 21 | :font "inherit" 22 | :outline "none" 23 | :resize "none"} 24 | :default-value "Auto-resizing\ntextarea" 25 | :placeholder "Auto-resizing textarea" 26 | :on-change (fn [_] (swap! state inc))}])) 27 | 28 | (mf/defc refs 29 | [] 30 | [:div 31 | [:& textarea]]) 32 | 33 | (defonce root 34 | (mf/create-root (dom/getElement "refs"))) 35 | 36 | (defn ^:after-load mount! [] 37 | (mf/render! root (mf/element refs))) 38 | -------------------------------------------------------------------------------- /examples/rumext/examples/timer_reactive.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.timer-reactive 2 | (:require 3 | [goog.dom :as dom] 4 | [rumext.v2 :as mf] 5 | [rumext.examples.util :as util])) 6 | 7 | (defonce components (atom {})) 8 | 9 | (mf/defc timer1 10 | {::mf/register-on components 11 | ::mf/forward-ref true} 12 | [props ref] 13 | (let [ts (mf/deref util/*clock)] 14 | [:div "Timer (deref)" ": " 15 | [:span {:style {:color @util/*color}} 16 | (util/format-time ts)]])) 17 | 18 | (mf/defc timer2 19 | {::mf/wrap [#(mf/throttle % 1000)]} 20 | [{:keys [ts] :as props}] 21 | [:div "Timer (props)" ": " 22 | [:span {:style {:color @util/*color}} 23 | (util/format-time ts)]]) 24 | 25 | (defonce root1 26 | (mf/create-root (dom/getElement "timer1"))) 27 | 28 | (defonce root2 29 | (mf/create-root (dom/getElement "timer2"))) 30 | 31 | (defn ^:after-load mount! [] 32 | (mf/render! root1 (mf/jsx timer1 {})) 33 | (mf/render! root2 (mf/jsx timer2 #js {:ts @util/*clock})) 34 | (add-watch util/*clock :timer-static 35 | (fn [_ _ _ ts] 36 | (mf/render! root2 (mf/jsx timer2 #js {:ts ts}))))) 37 | -------------------------------------------------------------------------------- /examples/rumext/examples/controls.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.controls 2 | (:require 3 | [goog.dom :as dom] 4 | [rumext.v2 :as mf] 5 | [rumext.examples.util :as util])) 6 | 7 | (mf/defc input* 8 | [{:keys [color] :as props}] 9 | (let [value (mf/deref color)] 10 | [:input {:type "text" 11 | :value value 12 | :style {:width 100} 13 | :on-change #(reset! color (.. % -target -value))}])) 14 | 15 | ;; Raw top-level component, everything interesting is happening inside 16 | (mf/defc controls* 17 | [props] 18 | [:dl 19 | [:dt "Color: "] 20 | [:dd 21 | [:> input* {:color util/*color}]] 22 | ;; Binding another component to the same atom will keep 2 input boxes in sync 23 | [:dt "Clone: "] 24 | [:dd 25 | (mf/jsx input* #js {:color util/*color})] 26 | [:dt "Color: "] 27 | [:dd {} (util/watches-count {:iref util/*color}) " watches"] 28 | 29 | [:dt "Tick: "] 30 | [:dd [:> input* {:color util/*speed}] " ms"] 31 | [:dt "Time:"] 32 | [:dd {} (util/watches-count {:iref util/*clock}) " watches"] 33 | ]) 34 | 35 | (defonce root 36 | (mf/create-root (dom/getElement "controls"))) 37 | 38 | (defn ^:after-load mount! [] 39 | (mf/render! root (mf/element controls*))) 40 | 41 | -------------------------------------------------------------------------------- /examples/rumext/examples/board.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.board 2 | (:require 3 | [goog.dom :as dom] 4 | [rumext.v2 :as mf] 5 | [rumext.examples.util :as util] 6 | [okulary.core :as l])) 7 | 8 | ;; Reactive drawing board 9 | 10 | (def board (atom (util/initial-board))) 11 | (def board-renders (atom 0)) 12 | 13 | (mf/defc cell 14 | [{:keys [x y] :as props}] 15 | (let [ref (mf/with-memo [x y] 16 | (l/derived (l/in [y x]) board)) 17 | cell (mf/deref ref) 18 | color (mf/deref util/*color)] 19 | [:div 20 | {:class "art-cell" 21 | :style {:background-color (when cell color)} 22 | :on-mouse-over (fn [_] (swap! board update-in [y x] not) nil)}])) 23 | 24 | (mf/defc board-reactive 25 | [] 26 | [:div.artboard 27 | (for [y (range 0 util/board-height)] 28 | [:div.art-row {:key y} 29 | (for [x (range 0 util/board-width)] 30 | (let [props #js {:key x :x x :y y}] 31 | ;; this is how one can specify React key for component 32 | [:& cell ^js props]))])]) 33 | 34 | (defonce root 35 | (mf/create-root (dom/getElement "board"))) 36 | 37 | (defn ^:after-load mount! [] 38 | (mf/render! root (mf/element board-reactive)) 39 | (js/setTimeout (fn [] 40 | (mf/render! root (mf/element board-reactive))) 41 | 2000)) 42 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {metosin/malli {:mvn/version "0.16.0"} 2 | funcool/cuerdas {:mvn/version "2023.11.09-407"} 3 | cljs-bean/cljs-bean {:mvn/version "1.9.0"}} 4 | :paths ["src"] 5 | :aliases 6 | {:dev 7 | {:extra-paths ["examples" "target" "test"] 8 | :extra-deps 9 | {com.bhauman/rebel-readline {:mvn/version "RELEASE"} 10 | funcool/okulary {:mvn/version "RELEASE"} 11 | thheller/shadow-cljs {:mvn/version "RELEASE"} 12 | org.clojure/tools.namespace {:mvn/version "RELEASE"} 13 | org.clojure/test.check {:mvn/version "RELEASE"} 14 | org.clojure/clojure {:mvn/version "RELEASE"} 15 | }} 16 | 17 | :codox 18 | {:extra-deps 19 | {codox/codox {:mvn/version "RELEASE"} 20 | org.clojure/tools.reader {:mvn/version "RELEASE"} 21 | codox-theme-rdash/codox-theme-rdash {:mvn/version "RELEASE"}}} 22 | 23 | :shadow-cljs 24 | {:main-opts ["-m" "shadow.cljs.devtools.cli"] 25 | :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} 26 | 27 | :repl 28 | {:main-opts ["-m" "rebel-readline.main"]} 29 | 30 | :outdated 31 | {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"} 32 | org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} 33 | :main-opts ["-m" "antq.core"]} 34 | 35 | :build 36 | {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} 37 | :ns-default build}}} 38 | -------------------------------------------------------------------------------- /examples/rumext/examples/util.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.util 2 | (:require 3 | [rumext.v2 :as mf] 4 | [goog.dom :as dom] 5 | [okulary.core :as l])) 6 | 7 | (defonce *clock (l/atom (.getTime (js/Date.)))) 8 | (defonce *color (l/atom "#FA8D97")) 9 | (defonce *speed (l/atom 160)) 10 | 11 | ;; Start clock ticking 12 | (defn tick [] 13 | (reset! *clock (.getTime (js/Date.)))) 14 | 15 | (defonce sem (js/setInterval tick @*speed)) 16 | 17 | (defn format-time [ts] 18 | (-> ts (js/Date.) (.toISOString) (subs 11 23))) 19 | 20 | (defn el [id] 21 | (dom/getElement id)) 22 | 23 | (mf/defc watches-count 24 | [{:keys [iref] :as props}] 25 | (let [state (mf/use-state 0)] 26 | (mf/with-effect [iref] 27 | (let [sem (js/setInterval #(swap! state inc) 1000)] 28 | #(do 29 | (js/clearInterval sem)))) 30 | 31 | [:span (.-size (.-watches ^js iref))])) 32 | 33 | ;; Generic board utils 34 | 35 | (def ^:const board-width 19) 36 | (def ^:const board-height 10) 37 | 38 | (defn prime? 39 | [i] 40 | (and (>= i 2) 41 | (empty? (filter #(= 0 (mod i %)) (range 2 i))))) 42 | 43 | (defn initial-board 44 | [] 45 | (->> (map prime? (range 0 (* board-width board-height))) 46 | (partition board-width) 47 | (mapv vec))) 48 | 49 | ;; (mf/def board-stats 50 | ;; :mixins [mf/reactive] 51 | ;; :render 52 | ;; (fn [own [*board *renders]] 53 | ;; [:div.stats 54 | ;; "Renders: " (mf/react *renders) 55 | ;; [:br] 56 | ;; "Board watches: " (watches-count *board) 57 | ;; [:br] 58 | ;; "Color watches: " (watches-count *color) ])) 59 | -------------------------------------------------------------------------------- /examples/rumext/examples/local_state.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.local-state 2 | (:require 3 | [goog.dom :as dom] 4 | [malli.core :as m] 5 | [rumext.v2 :as mf] 6 | [rumext.v2.util :as mfu] 7 | [rumext.examples.util :as util])) 8 | 9 | (def schema:label 10 | [:map {:title "label:props"} 11 | [:on-click {:optional true} fn?] 12 | [:my-id {:optional true} :keyword] 13 | [:title :string] 14 | [:n number?]]) 15 | 16 | (mf/defc label* 17 | {::mf/memo true 18 | ::mf/schema schema:label} 19 | [{:keys [class title n my-id] :as props :rest others}] 20 | (let [ref (mf/use-var nil) 21 | props (mf/spread-props others {:class (or class "my-label")})] 22 | 23 | (mf/with-effect [] 24 | (reset! ref 1)) 25 | 26 | [:> :div props 27 | [:span title ": " n]])) 28 | 29 | (mf/defc local-state 30 | "test docstring" 31 | {::mf/memo true 32 | ::mf/props :obj} 33 | [{:keys [title]}] 34 | (let [local (mf/use-state 35 | #(-> {:counter1 {:title "Counter 1" 36 | :n 0} 37 | :counter2 {:title "Counter 2" 38 | :n 0}}))] 39 | 40 | [:section {:class "counters" :style {:-webkit-border-radius "10px"}} 41 | [:hr] 42 | (let [{:keys [title n]} (:counter1 @local)] 43 | [:> label* {:n n :my-id "should-be-keyword" :title title :data-foobar 1 :on-click identity :id "foobar"}]) 44 | (let [{:keys [title n]} (:counter2 @local)] 45 | [:> label* {:title title :n n :on-click identity}]) 46 | [:button {:on-click #(swap! local update-in [:counter1 :n] inc)} "Increment Counter 1"] 47 | [:button {:on-click #(swap! local update-in [:counter2 :n] inc)} "Increment Counter 2"]])) 48 | 49 | (defonce root 50 | (mf/create-root (dom/getElement "local-state-1"))) 51 | 52 | (defn ^:after-load mount! [] 53 | (mf/render! root (mf/element local-state #js {:title "Clicks count"}))) 54 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | funcool 5 | rumext 6 | jar 7 | 2021.05.12-1 8 | rumext 9 | Simple and Decomplected UI library based on React. 10 | https://github.com/funcool/rumext 11 | 12 | 13 | MPL 2.0 14 | http://mozilla.org/MPL/2.0/ 15 | 16 | 17 | 18 | https://github.com/funcool/rumext 19 | scm:git:git://github.com/funcool/rumext.git 20 | scm:git:ssh://git@github.com/funcool/rumext.git 21 | master 22 | 23 | 24 | src 25 | 26 | 27 | 28 | clojars 29 | https://repo.clojars.org/ 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.clojure 38 | clojure 39 | 1.11.4 40 | 41 | 42 | cljs-bean 43 | cljs-bean 44 | 1.9.0 45 | 46 | 47 | funcool 48 | cuerdas 49 | 2023.11.09-407 50 | 51 | 52 | metosin 53 | malli 54 | 0.16.0 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/rumext/v2/validation.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016-2020 Andrey Antukh 6 | 7 | (ns ^:no-doc rumext.v2.validation 8 | "Runtime helpers" 9 | (:require 10 | [cuerdas.core :as str] 11 | [rumext.v2.util :as util] 12 | [malli.core :as m] 13 | [malli.transform :as mt] 14 | [malli.error :as me])) 15 | 16 | (def default-transformer mt/json-transformer) 17 | 18 | (defn process-explain-kv 19 | [prefix result k v] 20 | (let [nm (if (keyword? k) 21 | (name k) 22 | (str k)) 23 | pk (if prefix 24 | (str prefix "." nm) 25 | nm)] 26 | (cond 27 | (and (vector? v) (every? vector? v)) 28 | (let [data (into {} (map-indexed vector) v)] 29 | (reduce-kv (partial process-explain-kv pk) result data)) 30 | 31 | (and (vector? v) (every? map? v)) 32 | (let [gdata (into {} (comp 33 | (map :malli/error) 34 | (map-indexed vector) 35 | (filter second)) 36 | v) 37 | ndata (into {} (comp 38 | (map #(dissoc % :malli/error)) 39 | (map-indexed vector)) 40 | v) 41 | 42 | result (reduce-kv (partial process-explain-kv pk) result gdata) 43 | result (reduce-kv (partial process-explain-kv pk) result ndata)] 44 | 45 | result) 46 | 47 | (and (vector? v) (every? string? v)) 48 | (assoc result pk (peek v)) 49 | 50 | (map? v) 51 | (reduce-kv (partial process-explain-kv pk) result v) 52 | 53 | :else 54 | result))) 55 | 56 | (defn ^:no-doc validator 57 | [schema] 58 | (let [validator (delay (m/validator schema)) 59 | explainer (delay (m/explainer schema)) 60 | decoder (delay (m/decoder schema default-transformer))] 61 | (fn [props] 62 | (let [props (util/props-bean props) 63 | props (@decoder props) 64 | validate (deref validator)] 65 | (when-not ^boolean (^function validate props) 66 | (let [explainer (deref explainer) 67 | explain (^function explainer props) 68 | explain (me/humanize explain)] 69 | (reduce-kv (partial process-explain-kv nil) {} explain))))))) 70 | -------------------------------------------------------------------------------- /examples/rumext/examples/binary_clock.cljs: -------------------------------------------------------------------------------- 1 | (ns rumext.examples.binary-clock 2 | (:require 3 | [goog.dom :as dom] 4 | [rumext.v2 :as mf] 5 | [rumext.examples.util :as util])) 6 | 7 | (def *bclock-renders (atom 0)) 8 | 9 | (mf/defc render-count* 10 | [props] 11 | (let [renders (mf/deref *bclock-renders)] 12 | [:div.stats "Renders: " renders])) 13 | 14 | (mf/defc bit* 15 | [{:keys [n b]}] 16 | (mf/with-effect [n b] 17 | (swap! *bclock-renders inc)) 18 | 19 | (let [color (mf/deref util/*color)] 20 | (if (bit-test n b) 21 | [:td.bclock-bit {:style {:background-color color}}] 22 | [:td.bclock-bit {}]))) 23 | 24 | (mf/defc binary-clock* 25 | [] 26 | (let [ts (mf/deref util/*clock) 27 | msec (mod ts 1000) 28 | sec (mod (quot ts 1000) 60) 29 | min (mod (quot ts 60000) 60) 30 | hour (mod (quot ts 3600000) 24) 31 | hh (quot hour 10) 32 | hl (mod hour 10) 33 | mh (quot min 10) 34 | ml (mod min 10) 35 | sh (quot sec 10) 36 | sl (mod sec 10) 37 | msh (quot msec 100) 38 | msm (-> msec (quot 10) (mod 10)) 39 | msl (mod msec 10)] 40 | [:table.bclock 41 | [:tbody 42 | [:tr 43 | [:td] [:> bit* {:n hl :b 3}] [:th] 44 | [:td] [:> bit* {:n ml :b 3}] [:th] 45 | [:td] [:> bit* {:n sl :b 3}] [:th] 46 | [:> bit* {:n msh :b 3}] 47 | [:> bit* {:n msm :b 3}] 48 | [:> bit* {:n msl :b 3}]] 49 | [:tr 50 | [:td] [:> bit* {:n hl :b 2}] [:th] 51 | [:> bit* {:n mh :b 2}] 52 | [:> bit* {:n ml :b 2}] [:th] 53 | [:> bit* {:n sh :b 2}] 54 | [:> bit* {:n sl :b 2}] [:th] 55 | [:> bit* {:n msh :b 2}] 56 | [:> bit* {:n msm :b 2}] 57 | [:> bit* {:n msl :b 2}]] 58 | [:tr 59 | [:> bit* {:n hh :b 1}] 60 | [:> bit* {:n hl :b 1}] [:th] 61 | [:> bit* {:n mh :b 1}] 62 | [:> bit* {:n ml :b 1}] [:th] 63 | [:> bit* {:n sh :b 1}] 64 | [:> bit* {:n sl :b 1}] [:th] 65 | [:> bit* {:n msh :b 1}] 66 | [:> bit* {:n msm :b 1}] 67 | [:> bit* {:n msl :b 1}]] 68 | [:tr 69 | [:> bit* {:n hh :b 0}] 70 | [:> bit* {:n hl :b 0}] [:th] 71 | [:> bit* {:n mh :b 0}] 72 | [:> bit* {:n ml :b 0}] [:th] 73 | [:> bit* {:n sh :b 0}] 74 | [:> bit* {:n sl :b 0}] [:th] 75 | [:> bit* {:n msh :b 0}] 76 | [:> bit* {:n msm :b 0}] 77 | [:> bit* {:n msl :b 0}]] 78 | [:tr 79 | [:th hh] 80 | [:th hl] 81 | [:th] 82 | [:th mh] 83 | [:th ml] 84 | [:th] 85 | [:th sh] 86 | [:th sl] 87 | [:th] 88 | [:th msh] 89 | [:th msm] 90 | [:th msl]] 91 | [:tr 92 | [:th {:col-span 8} 93 | [:> render-count* {}]]]]])) 94 | 95 | (defonce root 96 | (mf/create-root (dom/getElement "binary-clock"))) 97 | 98 | (defn ^:after-load mount! [] 99 | (mf/render! root (mf/element binary-clock*))) 100 | 101 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rum test page 6 | 7 | 31 | 32 | 33 | 34 |
35 |
Local state
36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 |
Timers
44 |
45 |
46 |
47 |
48 | 49 |
50 |
Controls
51 |
52 |
53 | 54 |
55 |
Reactive binary clock
56 |
57 |
58 | 59 |
60 |
Reactive artboard
61 |
62 |
63 | 64 |
65 |
Inputs
66 |
67 |
68 | 69 |
70 |
Refs
71 |
72 |
73 | 74 |
75 |
Keys
76 |
77 |
78 | 79 |
80 |
BMI Calculator
81 |
82 |
83 | 84 |
85 |
Form validation
86 |
87 |
88 | 89 |
90 |
Self-reference
91 |
92 |
93 | 94 |
95 |
Contexts
96 |
97 |
98 | 99 |
100 |
Custom Methods and Data
101 |
102 |
103 | 104 |
105 |
Multiple Return
106 |
107 |
108 | 109 |
110 |
Portals
111 |
112 |
113 |
114 | 115 |
116 |
Error boundaries
117 |

Server:

118 |
119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/rumext/v2/normalize.clj: -------------------------------------------------------------------------------- 1 | (ns rumext.v2.normalize) 2 | 3 | (defn- element? 4 | "- is x a vector? 5 | AND 6 | - first element is a keyword?" 7 | [x] 8 | (and (vector? x) (keyword? (first x)))) 9 | 10 | (defn compact-map 11 | "Removes all map entries where the value of the entry is empty." 12 | [m] 13 | (reduce 14 | (fn [m k] 15 | (let [v (get m k)] 16 | (if (empty? v) 17 | (dissoc m k) m))) 18 | m (keys m))) 19 | 20 | (defn class-name 21 | [x] 22 | (cond 23 | (string? x) x 24 | (keyword? x) (name x) 25 | :else x)) 26 | 27 | (defn vec+stringify-class 28 | "Normalize `class` into a vector of classes (keywords will be stringified)." 29 | [klass] 30 | (cond 31 | (nil? klass) 32 | nil 33 | 34 | (list? klass) 35 | (if (symbol? (first klass)) 36 | [klass] 37 | (map class-name klass)) 38 | 39 | (symbol? klass) 40 | [klass] 41 | 42 | (string? klass) 43 | [klass] 44 | 45 | (keyword? klass) 46 | [(class-name klass)] 47 | 48 | (or (set? klass) 49 | (sequential? klass)) 50 | (mapv class-name klass) 51 | 52 | (map? klass) 53 | [klass] 54 | 55 | :else klass)) 56 | #_(vec+stringify-class :foo) 57 | 58 | (defn attributes 59 | "Normalize the :class, :class-name and :className elements" 60 | [attrs] 61 | (reduce (fn [attrs kw] 62 | (if-some [m (get attrs kw)] 63 | (-> attrs 64 | (dissoc kw) 65 | (update :class (fnil into []) (vec+stringify-class m))) 66 | attrs)) 67 | attrs [:class :className :class-name])) 68 | 69 | (defn merge-with-class 70 | "Like clojure.core/merge but concatenate :class entries." 71 | [m0 m1] 72 | (let [m0 (attributes m0) 73 | m1 (attributes m1) 74 | classes (into [] (comp (mapcat :class)) [m0 m1])] 75 | (cond-> (conj m0 m1) 76 | (not (empty? classes)) 77 | (assoc :class classes)))) 78 | #_(merge-with-class {:class "a"} {:class ["b"]}) 79 | 80 | (defn strip-css 81 | "Strip the # and . characters from the beginning of `s`." 82 | [s] 83 | (when (some? s) 84 | (cond 85 | (.startsWith s ".") (subs s 1) 86 | (.startsWith s "#") (subs s 1) 87 | :else s))) 88 | #_(strip-css "#foo") 89 | #_(strip-css ".foo") 90 | 91 | (defn match-tag 92 | "Match `s` as a CSS tag and return a vector of tag name, CSS id and 93 | CSS classes." 94 | [s] 95 | (let [matches (re-seq #"[#.]?[^#.]+" (subs (str s) 1)) 96 | [tag-name names] 97 | (cond (empty? matches) 98 | (throw (ex-info (str "Can't match CSS tag: " s) {:tag s})) 99 | (#{\# \.} (ffirst matches)) ;; shorthand for div 100 | ["div" matches] 101 | :default 102 | [(first matches) (rest matches)])] 103 | [(keyword tag-name) 104 | (first (map strip-css (filter #(= \# (first %1)) names))) 105 | (vec (map strip-css (filter #(= \. (first %1)) names)))])) 106 | #_(match-tag :.foo.bar#some-id) 107 | #_(match-tag :foo/span.foo.bar#some-id.hi) 108 | 109 | (defn children 110 | "Normalize the children of a HTML element." 111 | [x] 112 | (->> (cond 113 | (string? x) 114 | (list x) 115 | 116 | (element? x) 117 | (list x) 118 | 119 | (and (list? x) 120 | (symbol? x)) 121 | (list x) 122 | 123 | (list? x) 124 | x 125 | 126 | (and (sequential? x) 127 | (sequential? (first x)) 128 | (not (string? (first x))) 129 | (not (element? (first x))) 130 | (= (count x) 1)) 131 | (children (first x)) 132 | 133 | (sequential? x) 134 | x 135 | :else (list x)) 136 | (filterv some?))) 137 | 138 | (defn element 139 | "Given: 140 | [:div.x.y#id (other)] 141 | Returns: 142 | [:div {:id \"id\" 143 | :class [\"x\" \"y\"]} 144 | (other)]" 145 | [[tag & content]] 146 | (when (not (or (keyword? tag) (symbol? tag) (string? tag))) 147 | (throw (ex-info (str tag " is not a valid element name.") {:tag tag :content content}))) 148 | (let [[tag id klass] (match-tag tag) 149 | tag-attrs (compact-map {:id id :class klass}) 150 | map-attrs (first content)] 151 | (if (map? map-attrs) 152 | [tag 153 | (merge-with-class tag-attrs map-attrs) 154 | (children (next content))] 155 | [tag 156 | (attributes tag-attrs) 157 | (children content)]))) 158 | 159 | #_(element [:div#foo 'a]) 160 | #_(element [:div.a#foo]) 161 | #_(element [:h1.b {:className "a"}]) 162 | 163 | -------------------------------------------------------------------------------- /src/rumext/v2/util.cljc: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) 2016-2020 Andrey Antukh 6 | 7 | (ns ^:no-doc rumext.v2.util 8 | "Runtime helpers" 9 | (:require 10 | #?(:cljs [cljs-bean.core :as bean]) 11 | [cuerdas.core :as str] 12 | [malli.core :as m] 13 | [malli.error :as me])) 14 | 15 | (defn ident->key 16 | [nword] 17 | (let [nword (if (string? nword) nword (name nword))] 18 | (cond 19 | (nil? (str/index-of nword "-")) 20 | nword 21 | 22 | (str/starts-with? nword "-") 23 | (-> nword str/camel str/capital) 24 | 25 | :else 26 | (str/camel nword)))) 27 | 28 | (defn ident->prop 29 | "Compiles a keyword or symbol to string using react prop naming 30 | convention" 31 | [nword] 32 | (let [nword (if (string? nword) nword (name nword))] 33 | (cond 34 | (= nword "class") "className" 35 | (= nword "for") "htmlFor" 36 | (str/starts-with? nword "--") nword 37 | (str/starts-with? nword "data-") nword 38 | (str/starts-with? nword "aria-") nword 39 | :else 40 | (ident->key nword)))) 41 | 42 | #?(:cljs 43 | (defn obj->map 44 | [obj] 45 | (let [keys (.keys js/Object obj) 46 | len (alength keys)] 47 | (loop [i 0 48 | r (transient {})] 49 | (if (< i len) 50 | (let [key (aget keys i)] 51 | (recur (unchecked-inc i) 52 | (assoc! r (keyword key) (unchecked-get obj key)))) 53 | (persistent! r)))))) 54 | 55 | #?(:cljs 56 | (defn plain-object? 57 | ^boolean 58 | [o] 59 | (and (some? o) 60 | (identical? (.getPrototypeOf js/Object o) 61 | (.-prototype js/Object))))) 62 | 63 | #?(:cljs 64 | (defn map->obj 65 | [o] 66 | (cond 67 | (plain-object? o) 68 | o 69 | 70 | (map? o) 71 | (let [m #js {}] 72 | (run! (fn [[k v]] (unchecked-set m (name k) v)) o) 73 | m) 74 | 75 | :else 76 | (throw (ex-info "unable to create obj" {:data o}))))) 77 | 78 | #?(:cljs 79 | (defn map->props 80 | ([o] (map->props o false)) 81 | ([o recursive?] 82 | (if (object? o) 83 | o 84 | (let [level (if (true? recursive?) 1 recursive?)] 85 | (reduce-kv (fn [res k v] 86 | (let [v (if (keyword? v) (name v) v) 87 | k (cond 88 | (string? k) k 89 | (keyword? k) (if (and (int? level) (not= 1 level)) 90 | (ident->key k) 91 | (ident->prop k)) 92 | :else nil)] 93 | 94 | (when (some? k) 95 | (let [v (cond 96 | (and (= k "style") (map? v)) 97 | (map->props v true) 98 | 99 | (and (int? level) (map? v)) 100 | (map->props v (inc level)) 101 | 102 | :else 103 | v)] 104 | (unchecked-set res k v))) 105 | 106 | res)) 107 | #js {} 108 | o)))))) 109 | 110 | #?(:cljs 111 | (defn wrap-props 112 | [props] 113 | (cond 114 | (object? props) (obj->map props) 115 | (map? props) props 116 | (nil? props) {} 117 | :else (throw (ex-info "Unexpected props" {:props props}))))) 118 | 119 | #?(:cljs 120 | (defn props-equals? 121 | [eq? new-props old-props] 122 | (let [old-keys (.keys js/Object old-props) 123 | new-keys (.keys js/Object new-props) 124 | old-keys-len (alength old-keys) 125 | new-keys-len (alength new-keys)] 126 | (if (identical? old-keys-len new-keys-len) 127 | (loop [idx (int 0)] 128 | (if (< idx new-keys-len) 129 | (let [key (aget new-keys idx) 130 | new-val (unchecked-get new-props key) 131 | old-val (unchecked-get old-props key)] 132 | (if ^boolean (eq? new-val old-val) 133 | (recur (inc idx)) 134 | false)) 135 | true)) 136 | false)))) 137 | 138 | #?(:cljs 139 | (defn symbol-for 140 | [v] 141 | (.for js/Symbol v))) 142 | 143 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 144 | ;; BEANS 145 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 146 | 147 | #?(:cljs 148 | (defn prop->key 149 | [k] 150 | (if (string? k) 151 | (-> k str/kebab keyword) 152 | k))) 153 | 154 | #?(:cljs 155 | (defn react-prop->key 156 | [k] 157 | (if (string? k) 158 | (case k 159 | "htmlFor" :for 160 | "className" :class 161 | (-> k str/kebab keyword)) 162 | k))) 163 | 164 | #?(:cljs 165 | (defn- react-key->prop 166 | [x] 167 | (when (simple-keyword? x) 168 | (ident->prop (name x))))) 169 | 170 | #?(:cljs 171 | (defn- key->prop 172 | [x] 173 | (when (keyword? x) 174 | (str/camel (.-fqn ^cljs.core.Keyword x))))) 175 | 176 | #?(:cljs 177 | (defn bean 178 | [o] 179 | (bean/->clj o 180 | :prop->key prop->key 181 | :key->prop key->prop))) 182 | 183 | #?(:cljs 184 | (defn props-bean 185 | "A props specific bean that properly handles react props naming 186 | conventions" 187 | [o] 188 | (bean/->clj o 189 | :prop->key react-prop->key 190 | :key->prop react-key->prop))) 191 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # This file is generated by running "yarn install" inside your project. 2 | # Manual changes might be lost - proceed with caution! 3 | 4 | __metadata: 5 | version: 8 6 | cacheKey: 10c0 7 | 8 | "base64-js@npm:^1.3.1": 9 | version: 1.5.1 10 | resolution: "base64-js@npm:1.5.1" 11 | checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf 12 | languageName: node 13 | linkType: hard 14 | 15 | "buffer-from@npm:^1.0.0": 16 | version: 1.1.2 17 | resolution: "buffer-from@npm:1.1.2" 18 | checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 19 | languageName: node 20 | linkType: hard 21 | 22 | "buffer@npm:^6.0.3": 23 | version: 6.0.3 24 | resolution: "buffer@npm:6.0.3" 25 | dependencies: 26 | base64-js: "npm:^1.3.1" 27 | ieee754: "npm:^1.2.1" 28 | checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 29 | languageName: node 30 | linkType: hard 31 | 32 | "ieee754@npm:^1.2.1": 33 | version: 1.2.1 34 | resolution: "ieee754@npm:1.2.1" 35 | checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb 36 | languageName: node 37 | linkType: hard 38 | 39 | "isexe@npm:^3.1.1": 40 | version: 3.1.1 41 | resolution: "isexe@npm:3.1.1" 42 | checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 43 | languageName: node 44 | linkType: hard 45 | 46 | "process@npm:^0.11.10": 47 | version: 0.11.10 48 | resolution: "process@npm:0.11.10" 49 | checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 50 | languageName: node 51 | linkType: hard 52 | 53 | "react-dom@npm:19.1.0": 54 | version: 19.1.0 55 | resolution: "react-dom@npm:19.1.0" 56 | dependencies: 57 | scheduler: "npm:^0.26.0" 58 | peerDependencies: 59 | react: ^19.1.0 60 | checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc 61 | languageName: node 62 | linkType: hard 63 | 64 | "react@npm:19.1.0": 65 | version: 19.1.0 66 | resolution: "react@npm:19.1.0" 67 | checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 68 | languageName: node 69 | linkType: hard 70 | 71 | "readline-sync@npm:^1.4.10": 72 | version: 1.4.10 73 | resolution: "readline-sync@npm:1.4.10" 74 | checksum: 10c0/0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 75 | languageName: node 76 | linkType: hard 77 | 78 | "rumext@workspace:.": 79 | version: 0.0.0-use.local 80 | resolution: "rumext@workspace:." 81 | dependencies: 82 | process: "npm:^0.11.10" 83 | react: "npm:19.1.0" 84 | react-dom: "npm:19.1.0" 85 | shadow-cljs: "npm:3.1.3" 86 | languageName: unknown 87 | linkType: soft 88 | 89 | "scheduler@npm:^0.26.0": 90 | version: 0.26.0 91 | resolution: "scheduler@npm:0.26.0" 92 | checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 93 | languageName: node 94 | linkType: hard 95 | 96 | "shadow-cljs-jar@npm:1.3.4": 97 | version: 1.3.4 98 | resolution: "shadow-cljs-jar@npm:1.3.4" 99 | checksum: 10c0/c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 100 | languageName: node 101 | linkType: hard 102 | 103 | "shadow-cljs@npm:3.1.3": 104 | version: 3.1.3 105 | resolution: "shadow-cljs@npm:3.1.3" 106 | dependencies: 107 | buffer: "npm:^6.0.3" 108 | process: "npm:^0.11.10" 109 | readline-sync: "npm:^1.4.10" 110 | shadow-cljs-jar: "npm:1.3.4" 111 | source-map-support: "npm:^0.5.21" 112 | which: "npm:^5.0.0" 113 | ws: "npm:^8.18.1" 114 | bin: 115 | shadow-cljs: cli/runner.js 116 | checksum: 10c0/aee011854e0646b7b6f483b47cba573263477cd5b39bd5edb35830233cd7f6c2db4d98a629cdf003f81f9e4818f81fa00b5ccfa684dbf74156889c93ec80a666 117 | languageName: node 118 | linkType: hard 119 | 120 | "source-map-support@npm:^0.5.21": 121 | version: 0.5.21 122 | resolution: "source-map-support@npm:0.5.21" 123 | dependencies: 124 | buffer-from: "npm:^1.0.0" 125 | source-map: "npm:^0.6.0" 126 | checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d 127 | languageName: node 128 | linkType: hard 129 | 130 | "source-map@npm:^0.6.0": 131 | version: 0.6.1 132 | resolution: "source-map@npm:0.6.1" 133 | checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 134 | languageName: node 135 | linkType: hard 136 | 137 | "which@npm:^5.0.0": 138 | version: 5.0.0 139 | resolution: "which@npm:5.0.0" 140 | dependencies: 141 | isexe: "npm:^3.1.1" 142 | bin: 143 | node-which: bin/which.js 144 | checksum: 10c0/e556e4cd8b7dbf5df52408c9a9dd5ac6518c8c5267c8953f5b0564073c66ed5bf9503b14d876d0e9c7844d4db9725fb0dcf45d6e911e17e26ab363dc3965ae7b 145 | languageName: node 146 | linkType: hard 147 | 148 | "ws@npm:^8.18.1": 149 | version: 8.18.2 150 | resolution: "ws@npm:8.18.2" 151 | peerDependencies: 152 | bufferutil: ^4.0.1 153 | utf-8-validate: ">=5.0.2" 154 | peerDependenciesMeta: 155 | bufferutil: 156 | optional: true 157 | utf-8-validate: 158 | optional: true 159 | checksum: 10c0/4b50f67931b8c6943c893f59c524f0e4905bbd183016cfb0f2b8653aa7f28dad4e456b9d99d285bbb67cca4fedd9ce90dfdfaa82b898a11414ebd66ee99141e4 160 | languageName: node 161 | linkType: hard 162 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## Version v2.10 4 | 5 | - Add the `::rumext.v2/memo` metadata for simplify the common case for memoization 6 | - Add the `::rumext.v2.props/expect` metadata for props checking; it 7 | accepts a set of props for simple existence checkong or map with 8 | predicates for simple type checking 9 | - Add native js destructuring support on props 10 | 11 | 12 | ## Version v2.9.3 13 | 14 | - bugfixes 15 | 16 | ## Version v2.9.2 17 | 18 | - bugfixes 19 | 20 | ## Version v2.9.1 21 | 22 | - bugfixes 23 | 24 | 25 | ## Version v2.9 26 | 27 | - Make the library more lightweight removing unnecesary and duplicated code 28 | - Add the ability to define more react friendly components (for use 29 | outside cljs codebases). 30 | - Add the ability to define lazy loading components 31 | - Make `rumext.v2.compiler/compile-concat` public 32 | - Improve documentation 33 | 34 | 35 | ## Version v2.8 36 | 37 | - Export `React.lazy` as `lazy` helper 38 | - Add `lazy-component` macro that joins react lazy with shadow-cljs 39 | lazy loading 40 | 41 | ## Version v2.7 42 | 43 | - Update to react>=18 44 | 45 | 46 | ## Version v2.6 47 | 48 | - Bugfixes 49 | 50 | 51 | ## Version v2.5 52 | 53 | - Bugfixes 54 | 55 | 56 | ## Version v2.4 57 | 58 | - Add improve performance of internal css handling 59 | 60 | 61 | ## Version v2.3 62 | 63 | - Minor updates 64 | - Add with-fn macro 65 | 66 | ## Version v2.2 67 | 68 | - Add the ability to destructure js props 69 | 70 | ## Version v2.1 71 | 72 | - Make `use-id` available for react < 18. 73 | - Add `use-equal-memo` hook. 74 | - Add `use-debouce` hook. 75 | - Add experimental `use-ssr-effect`. 76 | 77 | ## Version v2.0 78 | 79 | - Change version numbering: simplified. 80 | - Add v2 namespace that compatible with React18 (still some warnings that will be addressed in next versions) 81 | 82 | 83 | ## Version 2022.04.19-148 84 | 85 | - Fix htmlFor attr handling 86 | 87 | 88 | ## Version 2022.04.19-147 89 | 90 | - Fix empty props handling on `[:&` handler 91 | - Minor optimizations with type hints 92 | - Remove unused code 93 | 94 | ## Version 2022.04.19-146 95 | 96 | - Fix throttle higher-order component 97 | 98 | 99 | ## Version 2022.04.19-145 100 | 101 | - Refactor jsx compiler to extract key before call jsx functions. 102 | - Fix race condition on `rumext.alpha/deref` hook. 103 | 104 | 105 | ## Version 2022.04.18-142 106 | 107 | - Minor fix on throttle and deferred higher-order components 108 | 109 | 110 | ## Version 2022.04.18-141 111 | 112 | - Fix `nil` props handling. 113 | 114 | 115 | ## Version 2022.04.18-140 116 | 117 | - More fixes related to jsx entry point changes. 118 | 119 | 120 | ## Version 2022.04.18-139 121 | 122 | - Fix void elements type `
` (bug introduced in prev version). 123 | 124 | ## Version 2022.04.18-138 125 | 126 | - Revert all react-18 changes (will be released as separated package). 127 | - Bundle simplified hicada compiler with simplier defaults. 128 | - Start using public api of JSX runtime instead of require the private production API. 129 | 130 | 131 | ## Version 2022.04.10-141 132 | 133 | - Fix key warnings. 134 | 135 | ## Version 2022.04.08-137 136 | 137 | - Use proper jsx runtime import 138 | 139 | 140 | ## Version 2022.04.08-135 141 | 142 | - Upgrade to react-18 143 | 144 | ## Version 2022.03.31-133 145 | 146 | - Avoid call internal deref on the deref hook. 147 | 148 | ## Version 2022.03.28-131 149 | 150 | - Make the state return value stable if the state value does not changes. 151 | - Allow use use-var return value on VDOM ref attrs. 152 | 153 | 154 | ## Version 2022.01.20.128 155 | 156 | - Dependencies updates 157 | - Add with-effect hook/macro. 158 | - Add with-memo hook/macro. 159 | 160 | ## Version 2021.05.12-1 161 | 162 | - Fix incompatibilities with hicada 0.1.9 163 | 164 | ## Version 2021.05.12-0 165 | 166 | - Fix bug in `adapt` with keywords. 167 | - Update hicada to 0.1.9 168 | 169 | ## Version 2021.01.26-0 170 | 171 | - Add `check-props` helper. 172 | 173 | 174 | ## Version 2020.11.27-0 175 | 176 | - Add `::mf/forward-ref` metadata and support for multiple arguments for components. 177 | 178 | 179 | ## Version 2020.10.14-1 180 | 181 | - Fix issues in previous release. 182 | 183 | 184 | ## Version 2020.10.14-0 185 | 186 | - Fix minor issues on previous version related 187 | to the optimized `create-element` function. 188 | 189 | 190 | ## Version 2020.10.06-0 191 | 192 | - Add highly optimized version of create-element. 193 | - Properly memoize result of use-var. 194 | - Update deps. 195 | 196 | 197 | ## Version 2020.08.21-0 198 | 199 | - Add `:rumext.alpha/register` and `:rumext.alpha/register-as` component metadata for automatically 200 | register the component on some atom. 201 | 202 | 203 | ## Version 2020.05.22-1 204 | 205 | - Bugfixes. 206 | 207 | ## Version 2020.05.22-0 208 | 209 | - Add context api. 210 | - Fix a memory leak warning on throttle higher-order component. 211 | 212 | 213 | ## Version 2020.05.04-0 214 | 215 | - Do not reverse wrappers. 216 | - Minor performance optimizations. 217 | - Add throttle higher-order component. 218 | - Add deferred higher-order component. 219 | - Update documentation. 220 | - Change license to MPL 2.0. 221 | 222 | 223 | ## Version 2020.04.14-1 224 | 225 | - Revert microtask changes. 226 | 227 | 228 | ## Version 2020.04.14-0 229 | 230 | - Schedule a microtask for adding watcher in `deref` hook. 231 | - Properly return value on use-var hook impl functions. 232 | 233 | 234 | ## Version 2020.04.11-0 235 | 236 | - Use `Symbol` instead of `gensym` on `deref` (faster and more 237 | compatible with `funcool/okulary`). 238 | - Expose `Profiler`. 239 | - Remove hydrante function. 240 | 241 | 242 | ## Version 2020.04.08-1 243 | 244 | - Fix component naming issues when wrap is used. 245 | 246 | 247 | ## Version 2020.04.02-3 248 | 249 | - Fix bugs with Fragments. 250 | 251 | 252 | ## Version 2020.04.02-2 253 | 254 | - Fix bugs on catch higher-order component. 255 | 256 | 257 | ## Version 2020.04.02-1 258 | 259 | - Fix bugs on use-memo and use-callback. 260 | - Fix bugs on catch higher-order component. 261 | 262 | 263 | ## Version 2020.04.01-3 264 | 265 | - Simplify `defc` and `fnc` macros. 266 | - Add `catch` higher-order error boundary component. 267 | - Rename `memo` to `memo'`. 268 | - Rename `wrap-memo` to `memo`. 269 | - Keep `wrap-memo` as backward compatible alias. 270 | 271 | 272 | 273 | ## Version 2020.04.01-2 274 | 275 | - Add `rumext.alpha/memo` as a raw variant of `wrap-memo`. 276 | 277 | 278 | ## Version 2020.04.01-1 279 | 280 | - Add `fnc` macro for define anonymous components (useful for define 281 | higher-order components). 282 | - Depend directrly from react and react-dom from npm. No more cljsjs packages. 283 | - Add printability for Symbol. 284 | 285 | 286 | ## Version 2020.03.24 287 | 288 | - Refactor hooks (make they almost 0 runtime cost). 289 | - Remove all old obsolete code. 290 | - Remove macros for define class based components. 291 | - Many performance improvements and code simplification. 292 | 293 | 294 | ## Version 2020.03.23 295 | 296 | - Complete rewrite. 297 | 298 | 299 | ## Version 1.0.0 300 | 301 | - Initial release. 302 | -------------------------------------------------------------------------------- /src/rumext/v2.cljs: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) Andrey Antukh 6 | 7 | (ns rumext.v2 8 | (:refer-clojure :exclude [ref deref use]) 9 | (:require-macros [rumext.v2 :refer [defc fnc]]) 10 | (:require 11 | ["react" :as react] 12 | ["react-dom" :as rdom] 13 | ["react-dom/client" :as rdomc] 14 | ["react/jsx-runtime" :as jsxrt] 15 | [cljs.core :as c] 16 | [goog.functions :as gf] 17 | [rumext.v2.util :as util] 18 | [rumext.v2.validation] 19 | [shadow.lazy])) 20 | 21 | (def ^:const undefined (js* "(void 0)")) 22 | 23 | (def browser-context? 24 | "A boolean var, indicates if the current code is running on browser main thread or not." 25 | (exists? js/window)) 26 | 27 | (def Component 28 | "The `react.Component` class" 29 | react/Component) 30 | 31 | (def Fragment 32 | "The `react.Fragment class" 33 | react/Fragment) 34 | 35 | (def Profiler 36 | "The `react.Profiler` class" 37 | react/Profiler) 38 | 39 | (def Suspense 40 | "The `react.Suspense` class" 41 | react/Suspense) 42 | 43 | (extend-type cljs.core.UUID 44 | INamed 45 | (-name [this] (js* "\"\" + ~{}" this)) 46 | (-namespace [_] "")) 47 | 48 | (def ^:no-doc ^function jsx jsxrt/jsx) 49 | (def ^:no-doc ^function jsxs jsxrt/jsxs) 50 | 51 | (defn merge-props 52 | [props1 props2] 53 | (js/Object.assign #js {} props1 props2)) 54 | 55 | (def ^function forward-ref 56 | "lets your component expose a DOM node to parent component with a ref." 57 | react/forwardRef) 58 | 59 | ;; --- Main Api 60 | 61 | (def ^function portal 62 | "Render `element` in a DOM `node` that is ouside of current DOM hierarchy." 63 | rdom/createPortal) 64 | 65 | (def ^function create-root 66 | "Creates react root" 67 | rdomc/createRoot) 68 | 69 | (def hydrate-root 70 | "Lets you display React components inside a browser DOM node whose 71 | HTML content was previously generated by react-dom/server" 72 | rdomc/hydrateRoot) 73 | 74 | (defn render! 75 | [root element] 76 | (.render ^js root element)) 77 | 78 | (defn unmount! 79 | "Removes component from the DOM tree." 80 | [root] 81 | (.unmount ^js root)) 82 | 83 | (def ^function create-ref react/createRef) 84 | 85 | (defn ref-val 86 | "Given state and ref handle, returns React component." 87 | [ref] 88 | (unchecked-get ref "current")) 89 | 90 | (defn set-ref-val! 91 | [ref val] 92 | (unchecked-set ref "current" val) 93 | val) 94 | 95 | (def ^function lazy 96 | "A helper for creating lazy loading components." 97 | react/lazy) 98 | 99 | ;; --- Context API 100 | 101 | (def ^function create-context 102 | "Create a react context" 103 | react/createContext) 104 | 105 | (defn provider 106 | "Get the current provider for specified context" 107 | [ctx] 108 | (unchecked-get ctx "Provider")) 109 | 110 | ;; --- Raw Hooks 111 | 112 | (def ^function useId 113 | "The `react.useId` hook function" 114 | react/useId) 115 | 116 | (def ^function useRef 117 | "The `react.useRef` hook function" 118 | react/useRef) 119 | 120 | (def ^function useState 121 | "The `react.useState` hook function" 122 | react/useState) 123 | 124 | (def ^function useEffect 125 | "The `react.useEffect` hook function" 126 | react/useEffect) 127 | 128 | (def ^function useInsertionEffect 129 | "The react.useInsertionEffect` hook function" 130 | react/useInsertionEffect) 131 | 132 | (def ^function useLayoutEffect 133 | "The `react.useLayoutEffect` hook function" 134 | react/useLayoutEffect) 135 | 136 | (def ^function useDeferredValue 137 | "The `react.useDeferredValue hook function" 138 | react/useDeferredValue) 139 | 140 | (def ^function useMemo 141 | "The `react.useMemo` hook function" 142 | react/useMemo) 143 | 144 | (def ^function useCallback 145 | "The `react.useCallback` hook function" 146 | react/useCallback) 147 | 148 | (def ^function useContext 149 | "The `react.useContext` hook function" 150 | react/useContext) 151 | 152 | (def ^function useTransition 153 | "The `react.useTransition` hook function" 154 | react/useTransition) 155 | 156 | ;; --- Hooks 157 | 158 | (def ^function use 159 | "The `react.use` helper" 160 | react/use) 161 | 162 | (def ^:private adapt-sym 163 | (js/Symbol "rumext:adapt-fn")) 164 | 165 | (unchecked-set (.-prototype cljs.core/UUID) 166 | adapt-sym 167 | (fn [o] (.-uuid ^cljs.core/UUID o))) 168 | 169 | (unchecked-set (.-prototype cljs.core/Keyword) 170 | adapt-sym 171 | (fn [o] (.toString ^js o))) 172 | 173 | (unchecked-set (.-prototype cljs.core/Symbol) 174 | adapt-sym 175 | (fn [o] (.toString ^js o))) 176 | 177 | (defn adapt 178 | [o] 179 | (when (some? o) 180 | (let [adapt-fn (unchecked-get o adapt-sym)] 181 | (if ^boolean adapt-fn 182 | (^function adapt-fn o) 183 | o)))) 184 | 185 | (defn deps 186 | "A helper for creating hook deps array, that handles some 187 | adaptations for clojure specific data types such that UUID and 188 | keywords" 189 | ([] #js []) 190 | ([a] #js [(adapt a)]) 191 | ([a b] #js [(adapt a) (adapt b)]) 192 | ([a b c] #js [(adapt a) (adapt b) (adapt c)]) 193 | ([a b c d] #js [(adapt a) (adapt b) (adapt c) (adapt d)]) 194 | ([a b c d e] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e)]) 195 | ([a b c d e f] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f)]) 196 | ([a b c d e f g] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g)]) 197 | ([a b c d e f g h] #js [(adapt a) (adapt b) (adapt c) (adapt d) (adapt e) (adapt f) (adapt g) (adapt h)]) 198 | ([a b c d e f g h & rest] (into-array (map adapt (into [a b c d e f g h] rest))))) 199 | 200 | (def ^function use-ref 201 | "A lisp-case alias for `useRef`" 202 | react/useRef) 203 | 204 | (def ^function use-ctx 205 | "A lisp-case short alias for the `useContext` hook function" 206 | react/useContext) 207 | 208 | (def ^function use-id 209 | "A lisp-case alias fro `useId` hook function" 210 | react/useId) 211 | 212 | (def ^function start-transition 213 | "An alias for react.startTransition function" 214 | react/startTransition) 215 | 216 | (def noop (constantly nil)) 217 | 218 | (defn use-effect 219 | "A rumext variant of the `useEffect` hook function with order of 220 | arguments inverted" 221 | ([f] (use-effect #js [] f)) 222 | ([deps f] 223 | (useEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) 224 | 225 | (defn use-insertion-effect 226 | "A rumext variant of the `useInsertionEffect` hook function with order 227 | of arguments inverted" 228 | ([f] (use-insertion-effect #js [] f)) 229 | ([deps f] 230 | (useInsertionEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) 231 | 232 | (defn use-layout-effect 233 | "A rumext variant of the `useLayoutEffect` hook function with order 234 | of arguments inverted" 235 | ([f] (use-layout-effect #js [] f)) 236 | ([deps f] 237 | (useLayoutEffect #(let [r (^function f)] (if (fn? r) r noop)) deps))) 238 | 239 | (defn use-ssr-effect 240 | "An EXPERIMENTAL use-effect version that detects if we are in a NON 241 | browser context and runs the effect fn inmediatelly." 242 | [deps effect-fn] 243 | (if ^boolean browser-context? 244 | (use-effect deps effect-fn) 245 | (let [ret (effect-fn)] 246 | (when (fn? ret) 247 | (ret))))) 248 | 249 | (defn use-memo 250 | "A rumext variant of the `useMemo` hook function with order 251 | of arguments inverted" 252 | ([f] (useMemo f #js [])) 253 | ([deps f] (useMemo f deps))) 254 | 255 | (defn use-transition 256 | "A rumext version of the `useTransition` hook function. Returns a 257 | function object that implements the IPending protocol for check the 258 | state of the transition." 259 | [] 260 | (let [tmp (useTransition) 261 | is-pending (aget tmp 0) 262 | start-fn (aget tmp 1)] 263 | (use-memo 264 | #js [is-pending] 265 | (fn [] 266 | (specify! (fn [cb-fn] 267 | (^function start-fn cb-fn)) 268 | cljs.core/IPending 269 | (-realized? [_] (not ^boolean is-pending))))))) 270 | 271 | (defn use-callback 272 | "A rumext variant of the `useCallback` hook function with order 273 | of arguments inverted" 274 | ([f] (useCallback f #js [])) 275 | ([deps f] (useCallback f deps))) 276 | 277 | (defn use-fn 278 | "A convenience short alias for `use-callback`" 279 | ([f] (useCallback f #js [])) 280 | ([deps f] (useCallback f deps))) 281 | 282 | (defn deref 283 | "A rumext hook for deref and watch an atom or atom like object. It 284 | internally uses the react.useSyncExternalSource API" 285 | [iref] 286 | (let [state (use-ref (c/deref iref)) 287 | key (use-id) 288 | get-state (use-fn #js [state] #(unchecked-get state "current")) 289 | subscribe (use-fn #js [iref key] 290 | (fn [listener-fn] 291 | (unchecked-set state "current" (c/deref iref)) 292 | (add-watch iref key (fn [_ _ _ newv] 293 | (unchecked-set state "current" newv) 294 | (^function listener-fn))) 295 | #(remove-watch iref key))) 296 | snapshot (use-fn #js [iref] #(c/deref iref))] 297 | (react/useSyncExternalStore subscribe get-state snapshot))) 298 | 299 | (deftype State [update-fn value] 300 | c/IReset 301 | (-reset! [_ value] 302 | (^function update-fn value)) 303 | 304 | c/ISwap 305 | (-swap! [self f] 306 | (^function update-fn f)) 307 | (-swap! [self f x] 308 | (^function update-fn #(f % x))) 309 | (-swap! [self f x y] 310 | (^function update-fn #(f % x y))) 311 | (-swap! [self f x y more] 312 | (^function update-fn #(apply f % x y more))) 313 | 314 | c/IDeref 315 | (-deref [_] value)) 316 | 317 | (defn use-state 318 | "A rumext variant of `useState`. Returns an object that implements 319 | the Atom protocols." 320 | ([] (use-state nil)) 321 | ([initial] 322 | (let [tmp (useState initial) 323 | ref (useRef nil) 324 | value (aget tmp 0) 325 | update-fn (aget tmp 1)] 326 | (use-memo #js [value] #(State. update-fn value))))) 327 | 328 | (defn use-var 329 | "A rumext custom hook that uses `useRef` under the hood. Returns an 330 | object that implements the Atom protocols. The updates does not 331 | trigger rerender." 332 | ([] (use-var nil)) 333 | ([initial] 334 | (let [ref (useRef nil)] 335 | (when (nil? (.-current ^js ref)) 336 | (let [self (fn [value] 337 | (let [target (unchecked-get ref "current")] 338 | (unchecked-set target "value" value)))] 339 | 340 | (unchecked-set self "value" initial) 341 | (unchecked-set ref "current" self) 342 | (specify! self 343 | c/IDeref 344 | (-deref [this] 345 | (.-value ^js this)) 346 | 347 | c/IReset 348 | (-reset! [this v] 349 | (unchecked-set this "value" v)) 350 | 351 | c/ISwap 352 | (-swap! 353 | ([this f] 354 | (unchecked-set this "value" (f (.-value ^js this)))) 355 | ([this f a] 356 | (unchecked-set this "value" (f (.-value ^js this) a))) 357 | ([this f a b] 358 | (unchecked-set this "value" (f (.-value ^js this) a b))) 359 | ([this f a b xs] 360 | (unchecked-set this "value" (apply f (.-value ^js this) a b xs))))))) 361 | 362 | (.-current ^js ref)))) 363 | 364 | ;; --- Other API 365 | 366 | (defn element 367 | "Create a react element. This is a public API for the internal `jsx` 368 | function" 369 | ([klass] 370 | (jsx klass #js {} undefined)) 371 | ([klass props] 372 | (let [props (cond 373 | (object? props) ^js props 374 | (map? props) (util/map->obj props) 375 | :else (throw (ex-info "Unexpected props" {:props props})))] 376 | (jsx klass props undefined)))) 377 | 378 | (def ^function create-element react/createElement) 379 | 380 | (def ^function element? react/isValidElement) 381 | 382 | ;; --- Higher-Order Components 383 | 384 | (defn memo 385 | "High order component for memoizing component props. Is a rumext 386 | variant of React.memo what accepts a value comparator 387 | function (instead of props comparator)" 388 | ([component] (react/memo component)) 389 | ([component eq?] 390 | (react/memo component #(util/props-equals? eq? %1 %2)))) 391 | 392 | (def ^function memo' 393 | "A raw variant of React.memo." 394 | react/memo) 395 | 396 | (def ^:private schedule 397 | (or (and (exists? js/window) js/window.requestAnimationFrame) 398 | #(js/setTimeout % 16))) 399 | 400 | (defn deferred 401 | "A higher-order component that just deffers the first render to the next tick" 402 | ([component] (deferred component schedule)) 403 | ([component sfn] 404 | (fnc deferred 405 | {::wrap-props false} 406 | [props] 407 | (let [tmp (useState false) 408 | render? (aget tmp 0) 409 | set-render (aget tmp 1)] 410 | (use-effect (fn [] (^function sfn #(^function set-render true)))) 411 | (when ^boolean render? 412 | [:> component props]))))) 413 | 414 | (defn throttle 415 | "A higher-order component that throttles the rendering" 416 | [component ms] 417 | (fnc throttle 418 | {::wrap-props false} 419 | [props] 420 | (let [tmp (useState props) 421 | state (aget tmp 0) 422 | set-state (aget tmp 1) 423 | 424 | ref (useRef false) 425 | render (useMemo 426 | #(gf/throttle 427 | (fn [v] 428 | (when-not ^boolean (ref-val ref) 429 | (^function set-state v))) 430 | ms) 431 | #js [])] 432 | (useEffect #(^function render props) #js [props]) 433 | (useEffect #(fn [] (set-ref-val! ref true)) #js []) 434 | [:> component state]))) 435 | 436 | (defn check-props 437 | "Utility function to use with `memo'`. 438 | Will check the `props` keys to see if they are equal. 439 | 440 | Usage: 441 | 442 | ```clojure 443 | (mf/defc my-component 444 | {::mf/wrap [#(mf/memo' % (mf/check-props [\"prop1\" \"prop2\"]))]} 445 | [props] 446 | ``` 447 | )" 448 | 449 | ([props] (check-props props =)) 450 | ([props eqfn?] 451 | (fn [np op] 452 | (every? #(eqfn? (unchecked-get np %) 453 | (unchecked-get op %)) 454 | props)))) 455 | 456 | (defn use-debounce 457 | "A rumext custom hook that debounces the value changes" 458 | [ms value] 459 | (let [[state update-fn] (useState value) 460 | update-fn (useMemo #(gf/debounce update-fn ms) #js [ms])] 461 | (useEffect #(update-fn value) #js [value]) 462 | state)) 463 | 464 | (defn use-equal-memo 465 | "A rumext custom hook that preserves object identity through using a 466 | `=` (value equality). Optionally, you can provide your own 467 | function." 468 | ([val] 469 | (let [ref (use-ref nil)] 470 | (when-not (= (ref-val ref) val) 471 | (set-ref-val! ref val)) 472 | (ref-val ref))) 473 | ([eqfn val] 474 | (let [ref (use-ref nil)] 475 | (when-not (eqfn (ref-val ref) val) 476 | (set-ref-val! ref val)) 477 | (ref-val ref)))) 478 | 479 | (def ^function use-deferred 480 | "A lisp-case shorter alias for `useDeferredValue`" 481 | react/useDeferredValue) 482 | 483 | (defn use-previous 484 | "A rumext custom hook that returns a value from previous render" 485 | [value] 486 | (let [ref (use-ref value)] 487 | (use-effect #js [value] #(set-ref-val! ref value)) 488 | (ref-val ref))) 489 | 490 | (defn use-update-ref 491 | "A rumext custom hook that updates the ref value if the value changes" 492 | [value] 493 | (let [ref (use-ref value)] 494 | (use-effect #js [value] #(set-ref-val! ref value)) 495 | ref)) 496 | 497 | (defn use-ref-fn 498 | "A rumext custom hook that returns a stable callback pointer what 499 | calls the interned callback. The interned callback will be 500 | automatically updated on each render if the reference changes and 501 | works as noop if the pointer references to nil value." 502 | [f] 503 | (let [ptr (use-ref nil)] 504 | (use-effect #js [f] #(set-ref-val! ptr f)) 505 | (use-fn (fn [] 506 | (let [f (ref-val ptr) 507 | args (js-arguments)] 508 | (when (some? f) 509 | (.apply f args))))))) 510 | 511 | 512 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /src/rumext/v2.clj: -------------------------------------------------------------------------------- 1 | ;; This Source Code Form is subject to the terms of the Mozilla Public 2 | ;; License, v. 2.0. If a copy of the MPL was not distributed with this 3 | ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | ;; 5 | ;; Copyright (c) Andrey Antukh 6 | 7 | (ns rumext.v2 8 | (:refer-clojure :exclude [simple-ident?]) 9 | (:require 10 | [cljs.core :as-alias c] 11 | [clojure.string :as str] 12 | [rumext.v2.compiler :as hc] 13 | [rumext.v2.util :as util])) 14 | 15 | (create-ns 'rumext.v2.util) 16 | 17 | (defn ^:no-doc production-build? 18 | [] 19 | (let [env (System/getenv)] 20 | (or (= "production" (get env "NODE_ENV")) 21 | (= "production" (get env "RUMEXT_ENV")) 22 | (= "production" (get env "TARGET_ENV"))))) 23 | 24 | (defmacro html 25 | [body] 26 | (hc/compile body)) 27 | 28 | (defn- without-qualified 29 | [data] 30 | (reduce-kv (fn [data k _] 31 | (if (qualified-keyword? k) 32 | (dissoc data k) 33 | data)) 34 | data 35 | data)) 36 | 37 | (defn- without-nils 38 | [data] 39 | (reduce-kv (fn [data k v] 40 | (if (nil? v) 41 | (dissoc data k) 42 | data)) 43 | data 44 | data)) 45 | 46 | (defn parse-defc 47 | [args] 48 | (loop [r {} 49 | s 0 50 | v (first args) 51 | n (rest args)] 52 | (case s 53 | 0 (if (symbol? v) 54 | (recur (assoc r :cname v) (inc s) (first n) (rest n)) 55 | (recur (assoc r :cname (gensym "anonymous-")) (inc s) v n)) 56 | 1 (if (string? v) 57 | (recur (assoc r :doc v) (inc s) (first n) (rest n)) 58 | (recur r (inc s) v n)) 59 | 2 (if (map? v) 60 | (recur (assoc r :meta v) (inc s) (first n) (rest n)) 61 | (recur r (inc s) v n)) 62 | 3 (if (vector? v) 63 | (recur (assoc r :args v) (inc s) (first n) (rest n)) 64 | (throw (ex-info "Invalid macro definition: expected component args vector" {}))) 65 | 66 | (let [psym (with-meta (gensym "props-") {:tag 'js}) 67 | meta (get r :meta) 68 | fmeta (-> (without-qualified meta) 69 | (assoc :doc (some-> r :doc str)) 70 | (assoc :lazy-loadable (get meta ::lazy-load)) 71 | (assoc :private (get meta ::private)) 72 | (without-nils))] 73 | 74 | {:cname (:cname r) 75 | :props (first (:args r)) 76 | :params (into [psym] (rest (:args r))) 77 | :body (cons v n) 78 | :psym psym 79 | :fmeta fmeta 80 | :meta meta})))) 81 | 82 | (defn- wrap-props? 83 | [{:keys [cname meta]}] 84 | (let [default-style (if (str/ends-with? (name cname) "*") :obj :clj)] 85 | (cond 86 | (contains? meta ::props) 87 | (= :clj (get meta ::props default-style)) 88 | 89 | (contains? meta ::wrap-props) 90 | (get meta ::wrap-props) 91 | 92 | (str/ends-with? (name cname) "*") 93 | false 94 | 95 | :else 96 | true))) 97 | 98 | (defn- react-props? 99 | [{:keys [meta cname] :as ctx}] 100 | (and (not (wrap-props? ctx)) 101 | (or (str/ends-with? (name cname) "*") 102 | (= (::props meta) :react)))) 103 | 104 | (defn- simple-ident? 105 | [s] 106 | (some? (re-matches #"[A-Za-z0-9_]+" s))) 107 | 108 | (defn- prepare-let-bindings 109 | [{:keys [cname meta props body params] :as ctx}] 110 | (let [react-props? (react-props? ctx) 111 | psym (first params)] 112 | (cond 113 | (and (some? props) (wrap-props? ctx)) 114 | [props (list 'rumext.v2.util/wrap-props psym)] 115 | 116 | (and (map? props) (not (wrap-props? ctx))) 117 | (let [alias (get props :as) 118 | alts (get props :or) 119 | other (or (get props :&) 120 | (get props :rest)) 121 | items (some-> (get props :keys) set)] 122 | (cond->> [] 123 | (symbol? alias) 124 | (into [alias psym]) 125 | 126 | (symbol? other) 127 | (into [other (list 'js* "undefined")]) 128 | 129 | (set? items) 130 | (concat (mapcat (fn [k] 131 | (let [prop-name (if react-props? 132 | (util/ident->prop k) 133 | (name k)) 134 | accessor (if (simple-ident? prop-name) 135 | (list '. psym (symbol (str "-" prop-name))) 136 | (list 'cljs.core/unchecked-get psym prop-name))] 137 | 138 | [(if (symbol? k) k (symbol prop-name)) 139 | (cond 140 | ;; If the other symbol is present, then a 141 | ;; different destructuring stragegy will be 142 | ;; used so we need to set here the value to 143 | ;; 'undefined' 144 | (symbol? other) 145 | (list 'js* "undefined") 146 | 147 | (contains? alts k) 148 | `(~'js* "~{} ?? ~{}" ~accessor ~(get alts k)) 149 | 150 | :else 151 | accessor)])) 152 | items)))) 153 | 154 | (symbol? props) 155 | [props psym]))) 156 | 157 | (defn native-destructure 158 | "Generates a js var line with native destructuring. Only used when :& 159 | used in destructuring." 160 | [{:keys [props params props] :as ctx}] 161 | 162 | ;; Emit native destructuring only if the :& key has value 163 | (when (or (symbol? (:& props)) 164 | (symbol? (:rest props))) 165 | 166 | (let [react-props? (react-props? ctx) 167 | psym (first params) 168 | 169 | keys-props (:keys props []) 170 | all-alias (:as props) 171 | rst-alias (or (:& props) (:rest props)) 172 | 173 | k-props (dissoc props :keys :as :& :rest) 174 | k-props (->> (:keys props []) 175 | (map (fn [k] 176 | (let [kv (if react-props? 177 | (util/ident->prop k) 178 | (name k))] 179 | [k kv]))) 180 | (into k-props)) 181 | 182 | props [] 183 | params [] 184 | 185 | [props params] 186 | (if (seq k-props) 187 | (reduce (fn [[props params] [ks kp]] 188 | (let [kp (if react-props? 189 | (util/ident->prop kp) 190 | (name kp))] 191 | [(conj props (str "~{}: ~{}")) 192 | (conj params kp ks)])) 193 | [props params] 194 | k-props) 195 | [props params]) 196 | 197 | [props params] 198 | (if (symbol? rst-alias) 199 | [(conj props "...~{}") (conj params rst-alias)] 200 | [props params]) 201 | 202 | tmpl (str "var {" 203 | (str/join ", " props) 204 | "} = ~{}") 205 | params (conj params psym)] 206 | 207 | [(apply list 'js* tmpl params)]))) 208 | 209 | (defn- prepare-props-checks 210 | [{:keys [cname meta params] :as ctx}] 211 | (let [react-props? (react-props? ctx) 212 | psym (vary-meta (first params) assoc :tag 'js)] 213 | (when *assert* 214 | (cond 215 | (::schema meta) 216 | (let [validator-sym (with-meta (symbol (str cname "-validator")) 217 | {:tag 'function})] 218 | (concat 219 | (cons (list 'js* "// ===== start props checking =====") nil) 220 | [`(let [res# (~validator-sym ~psym)] 221 | (when (some? res#) 222 | (let [items# (reduce-kv (fn [result# k# v#] 223 | (conj result# (str " -> '" k# "' " v# ""))) 224 | [] 225 | res#) 226 | msg# (str ~(str "invalid props on component " (str cname) "\n\n") 227 | (str/join "\n" items#) 228 | "\n")] 229 | (throw (js/Error. msg#)))))] 230 | (cons (list 'js* "// ===== end props checking =====") nil))) 231 | 232 | (::expect meta) 233 | (let [props (::expect meta)] 234 | (concat 235 | (cons (list 'js* "// ===== start props checking =====") nil) 236 | (if (map? props) 237 | (->> props 238 | (map (fn [[prop pred-sym]] 239 | (let [prop (if react-props? 240 | (util/ident->prop prop) 241 | (name prop)) 242 | 243 | accs (if (simple-ident? prop) 244 | (list '. psym (symbol (str "-" prop))) 245 | (list 'cljs.core/unchecked-get psym prop)) 246 | 247 | expr `(~pred-sym ~accs)] 248 | `(when-not ~(vary-meta expr assoc :tag 'boolean) 249 | (throw (js/Error. ~(str "invalid value for '" prop "'")))))))) 250 | 251 | (->> props 252 | (map (fn [prop] 253 | (let [prop (if react-props? 254 | (util/ident->prop prop) 255 | (name prop)) 256 | expr `(.hasOwnProperty ~psym ~prop)] 257 | `(when-not ~(vary-meta expr assoc :tag 'boolean) 258 | (throw (js/Error. ~(str "missing prop '" prop "'"))))))))) 259 | (cons (list 'js* "// ===== end props checking =====") nil))) 260 | 261 | :else 262 | [])))) 263 | 264 | (defn- prepare-render-fn 265 | [{:keys [cname meta body params props] :as ctx}] 266 | (let [f `(fn ~cname ~params 267 | ~@(prepare-props-checks ctx) 268 | (let [~@(prepare-let-bindings ctx)] 269 | ~@(native-destructure ctx) 270 | 271 | ~@(butlast body) 272 | ~(hc/compile (last body))))] 273 | (if (::forward-ref meta) 274 | `(rumext.v2/forward-ref ~f) 275 | f))) 276 | 277 | (defn- resolve-wrappers 278 | [{:keys [cname meta] :as ctx}] 279 | (let [wrappers (or (::wrap meta) (:wrap meta) []) 280 | react-props? (react-props? ctx) 281 | memo (::memo meta)] 282 | (cond 283 | (set? memo) 284 | (let [eq-f (or (::memo-equals ctx) 'cljs.core/=) 285 | np-s (with-meta (gensym "new-props-") {:tag 'js}) 286 | op-s (with-meta (gensym "old-props-") {:tag 'js}) 287 | op-f (fn [prop] 288 | (let [prop (if react-props? 289 | (util/ident->prop prop) 290 | (name prop)) 291 | accs (if (simple-ident? prop) 292 | (let [prop (symbol (str "-" (name prop)))] 293 | (list eq-f 294 | (list '.. np-s prop) 295 | (list '.. op-s prop))) 296 | (list eq-f 297 | (list 'cljs.core/unchecked-get np-s prop) 298 | (list 'cljs.core/unchecked-get op-s prop)))] 299 | (with-meta accs {:tag 'boolean})))] 300 | (conj wrappers 301 | `(fn [component#] 302 | (mf/memo' component# (fn [~np-s ~op-s] 303 | (and ~@(map op-f memo))))))) 304 | 305 | (true? memo) 306 | (if-let [eq-f (::memo-equals meta)] 307 | (conj wrappers `(fn [component#] 308 | (mf/memo component# ~eq-f))) 309 | (conj wrappers 'rumext.v2/memo')) 310 | 311 | :else wrappers))) 312 | 313 | (defmacro fnc 314 | "A macro for defining inline component functions. Look the user guide for 315 | understand how to use it." 316 | [& args] 317 | (let [{:keys [cname meta] :as ctx} (parse-defc args) 318 | wrappers (resolve-wrappers ctx) 319 | rfs (gensym (str cname "__"))] 320 | `(let [~rfs ~(if (seq wrappers) 321 | (reduce (fn [r fi] `(~fi ~r)) (prepare-render-fn ctx) wrappers) 322 | (prepare-render-fn ctx))] 323 | ~@(when-not (production-build?) 324 | [`(set! (.-displayName ~rfs) ~(str cname))]) 325 | ~rfs))) 326 | 327 | (defmacro defc 328 | "A macro for defining component functions. Look the user guide for 329 | understand how to use it." 330 | [& args] 331 | (let [{:keys [cname fmeta meta] :as ctx} (parse-defc args) 332 | wrappers (resolve-wrappers ctx) 333 | react-props? (react-props? ctx) 334 | cname (with-meta cname fmeta)] 335 | `(do 336 | ~@(when (and (::schema meta) react-props? *assert*) 337 | (let [validator-sym (with-meta (symbol (str cname "-validator")) 338 | {:tag 'function})] 339 | [`(def ~validator-sym (rumext.v2.validation/validator ~(::schema meta)))])) 340 | 341 | (def ~cname ~(if (seq wrappers) 342 | (reduce (fn [r fi] `(~fi ~r)) (prepare-render-fn ctx) wrappers) 343 | (prepare-render-fn ctx))) 344 | 345 | ~@(when-not (production-build?) 346 | [`(set! (.-displayName ~cname) ~(str cname))]) 347 | 348 | ~(when-let [registry (::register meta)] 349 | `(swap! ~registry (fn [state#] (assoc state# ~(::register-as meta (keyword (str cname))) ~cname))))))) 350 | 351 | (defmacro deps 352 | "A convenience macro version of mf/deps function" 353 | [& params] 354 | `(cljs.core/array ~@(map (fn [s] `(rumext.v2/adapt ~s)) params))) 355 | 356 | (defmacro with-memo 357 | "A convenience syntactic abstraction (macro) for `useMemo`" 358 | [deps & body] 359 | (cond 360 | (vector? deps) 361 | `(rumext.v2/use-memo 362 | (rumext.v2/deps ~@deps) 363 | (fn [] ~@body)) 364 | 365 | 366 | (nil? deps) 367 | `(rumext.v2/use-memo 368 | nil 369 | (fn [] ~@body)) 370 | 371 | :else 372 | `(rumext.v2/use-memo 373 | (fn [] ~@(cons deps body))))) 374 | 375 | (defmacro ^:no-doc with-fn 376 | [deps & body] 377 | (cond 378 | (vector? deps) 379 | `(rumext.v2/use-fn 380 | (rumext.v2/deps ~@deps) 381 | ~@body) 382 | 383 | 384 | (nil? deps) 385 | `(rumext.v2/use-fn 386 | nil 387 | ~@body) 388 | 389 | :else 390 | `(rumext.v2/use-fn 391 | ~@(cons deps body)))) 392 | 393 | (defmacro with-effect 394 | "A convenience syntactic abstraction (macro) for `useEffect`" 395 | [deps & body] 396 | (cond 397 | (vector? deps) 398 | `(rumext.v2/use-effect 399 | (rumext.v2/deps ~@deps) 400 | (fn [] ~@body)) 401 | 402 | (nil? deps) 403 | `(rumext.v2/use-effect 404 | nil 405 | (fn [] ~@body)) 406 | 407 | :else 408 | `(rumext.v2/use-effect 409 | (fn [] ~@(cons deps body))))) 410 | 411 | (defmacro with-layout-effect 412 | "A convenience syntactic abstraction (macro) for `useLayoutEffect`" 413 | [deps & body] 414 | (cond 415 | (vector? deps) 416 | `(rumext.v2/use-layout-effect 417 | (rumext.v2/deps ~@deps) 418 | (fn [] ~@body)) 419 | 420 | (nil? deps) 421 | `(rumext.v2/use-layout-effect 422 | nil 423 | (fn [] ~@body)) 424 | 425 | :else 426 | `(rumext.v2/use-layout-effect 427 | (fn [] ~@(cons deps body))))) 428 | 429 | (defmacro check-props 430 | "A macro version of the `check-props` function" 431 | [props & [eq-f :as rest]] 432 | (if (symbol? props) 433 | `(apply rumext.v2/check-props ~props ~rest) 434 | 435 | (let [eq-f (or eq-f 'cljs.core/=) 436 | np-s (with-meta (gensym "new-props-") {:tag 'js}) 437 | op-s (with-meta (gensym "old-props-") {:tag 'js}) 438 | op-f (fn [prop] 439 | (let [prop-access (symbol (str "-" (name prop)))] 440 | (with-meta 441 | (if (simple-ident? prop) 442 | (list eq-f 443 | (list '.. np-s prop-access) 444 | (list '.. op-s prop-access)) 445 | (list eq-f 446 | (list 'cljs.core/unchecked-get np-s prop) 447 | (list 'cljs.core/unchecked-get op-s prop))) 448 | {:tag 'boolean})))] 449 | `(fn [~np-s ~op-s] 450 | (and ~@(map op-f props)))))) 451 | 452 | (defmacro lazy-component 453 | "A macro that helps defining lazy-loading components with the help 454 | of shadow-cljs tooling." 455 | [ns-sym] 456 | (if (production-build?) 457 | `(let [loadable# (shadow.lazy/loadable ~ns-sym)] 458 | (rumext.v2/lazy (fn [] 459 | (.then (shadow.lazy/load loadable#) 460 | (fn [component#] 461 | (cljs.core/js-obj "default" component#)))))) 462 | 463 | `(let [loadable# (shadow.lazy/loadable ~ns-sym)] 464 | (rumext.v2/lazy (fn [] 465 | (.then (shadow.lazy/load loadable#) 466 | (fn [_#] 467 | (cljs.core/js-obj "default" 468 | (rumext.v2/fnc ~'wrapper 469 | {:rumext.v2/props :obj} 470 | [props#] 471 | [:> (deref loadable#) props#]))))))))) 472 | 473 | (defmacro spread-object 474 | "A helper for spread two js objects, adapting compile time known 475 | keys to cameCase. 476 | 477 | You can pass `:rumext.v2/transform false` on `other` metadata 478 | for disable key casing transformation." 479 | [target other] 480 | (assert (or (symbol? target) 481 | (map? target)) 482 | "only symbols or maps accepted on target") 483 | 484 | (assert (or (symbol? other) 485 | (map? other)) 486 | "only symbols or map allowed for the spread") 487 | 488 | (let [transform? (get (meta other) ::transform true) 489 | compile-prop (if transform? 490 | (partial hc/compile-prop 2) 491 | identity)] 492 | (hc/compile-to-js-spread target other compile-prop))) 493 | 494 | (defmacro spread-props 495 | "A helper for spread two js objects using react conventions for 496 | compile time known props keys names." 497 | [target other] 498 | (assert (or (symbol? target) 499 | (map? target)) 500 | "only symbols or maps accepted on target") 501 | 502 | (assert (or (symbol? other) 503 | (map? other)) 504 | "only symbols or map allowed for the spread") 505 | 506 | (hc/compile-to-js-spread target other hc/compile-prop)) 507 | 508 | (defmacro spread 509 | "A shorter alias for spread props" 510 | [target other] 511 | `(spread-props ~target ~other)) 512 | 513 | (defmacro props 514 | "A helper for convert literal datastructures into js data 515 | structures at compile time using react props convention." 516 | [value] 517 | (let [recursive? (get (meta value) ::recursive false)] 518 | (hc/compile-props-to-js value ::hc/transform-props-recursive recursive?))) 519 | 520 | (defmacro object 521 | [value] 522 | (let [recursive? (get (meta value) ::recursive true)] 523 | (hc/compile-coll-to-js value ::hc/transform-props-recursive recursive?))) 524 | -------------------------------------------------------------------------------- /src/rumext/v2/compiler.clj: -------------------------------------------------------------------------------- 1 | ;; TODO: move to .CLJ file 2 | 3 | (ns rumext.v2.compiler 4 | " 5 | Hicada - Hiccup compiler aus dem Allgaeu 6 | 7 | NOTE: The code for has been forked like this: 8 | weavejester/hiccup -> r0man/sablono -> Hicada -> rumext" 9 | (:refer-clojure :exclude [compile]) 10 | (:require 11 | [clojure.core :as c] 12 | [clojure.string :as str] 13 | [rumext.v2 :as-alias mf] 14 | [rumext.v2.normalize :as norm] 15 | [rumext.v2.util :as util]) 16 | (:import 17 | cljs.tagged_literals.JSValue)) 18 | 19 | (declare ^:private compile*) 20 | (declare ^:private compile-map-to-js) 21 | (declare ^:private compile-prop) 22 | (declare ^:private compile-to-js) 23 | (declare ^:private compile-vec-to-js) 24 | (declare ^:private emit-jsx) 25 | 26 | (def ^:dynamic *transform-props-recursive* nil) 27 | (def ^:dynamic *handlers* nil) 28 | 29 | (defn- js-value? 30 | [o] 31 | (instance? JSValue o)) 32 | 33 | (defn- valid-props-type? 34 | [o] 35 | (or (symbol? o) 36 | (js-value? o) 37 | (seq? o) 38 | (nil? o) 39 | (map? o))) 40 | 41 | (def default-handlers 42 | {:> (fn [& [_ tag props :as children]] 43 | (when (> 2 (count children)) 44 | (throw (ex-info "invalid params for `:>` handler, tag and props are mandatory" 45 | {:params children}))) 46 | 47 | (let [props (or props {}) 48 | props (if (instance? clojure.lang.IObj props) 49 | (let [mdata (meta props)] 50 | (vary-meta props assoc 51 | ::handler :> 52 | ::transform-props-keys true 53 | ::transform-props-recursive (get mdata ::mf/recursive false))) 54 | props)] 55 | [tag props (drop 3 children)])) 56 | 57 | :>> (fn [& [_ tag props :as children]] 58 | (when (> 3 (count children)) 59 | (throw (ex-info "invalid params for `:>` handler, tag and props are mandatory" 60 | {:params children}))) 61 | 62 | (let [props (or props {}) 63 | props (if (instance? clojure.lang.IObj props) 64 | (vary-meta props assoc 65 | ::handler :>> 66 | ::transform-props-keys true 67 | ::transform-props-recursive true) 68 | props)] 69 | [tag props (drop 3 children)])) 70 | 71 | :& (fn [& [_ tag props :as children]] 72 | (when (> 2 (count children)) 73 | (throw (ex-info "invalid params for `:&` handler, tag and props are mandatory" 74 | {:params children}))) 75 | 76 | (when-not (valid-props-type? props) 77 | (throw (ex-info "invalid props type: obj, symbol seq or map is allowed" 78 | {:props props}))) 79 | 80 | (let [props (or props {}) 81 | props (vary-meta props assoc 82 | ::handler :& 83 | ::transform-props-keys false 84 | ::transform-props-recursive false 85 | ::allow-dynamic-transform true)] 86 | [tag props (drop 3 children)])) 87 | 88 | :? (fn [& [_ props :as children]] 89 | (if (map? props) 90 | ['rumext.v2/Suspense props (drop 2 children)] 91 | ['rumext.v2/Suspense {} (drop 1 children)])) 92 | 93 | :* (fn [& [_ props :as children]] 94 | (if (map? props) 95 | ['rumext.v2/Fragment props (drop 2 children)] 96 | ['rumext.v2/Fragment {} (drop 1 children)]))}) 97 | 98 | (defn- unevaluated? 99 | "True if the expression has not been evaluated. 100 | - expr is a symbol? OR 101 | - it's something like (foo bar)" 102 | [expr] 103 | (or (symbol? expr) 104 | (and (seq? expr) 105 | (not= (first expr) `quote)))) 106 | 107 | (defn- literal? 108 | "True if x is a literal value that can be rendered as-is." 109 | [x] 110 | (and (not (unevaluated? x)) 111 | (or (not (or (vector? x) (map? x))) 112 | (and (every? literal? x) 113 | (not (keyword? (first x))))))) 114 | 115 | (defn- join-classes 116 | "Join the `classes` with a whitespace." 117 | [classes] 118 | (->> (map #(if (string? %) % (seq %)) classes) 119 | (flatten) 120 | (remove nil?) 121 | (str/join " "))) 122 | 123 | (defn compile-concat 124 | "Compile efficient and performant string concatenation operation" 125 | [params & {:keys [safe?]}] 126 | (let [xform (comp (filter some?) 127 | (if safe? 128 | (map (fn [part] 129 | (if (string? part) 130 | part 131 | (list 'js* "(~{} ?? \"\")" part)))) 132 | (map identity))) 133 | params (into [] xform params)] 134 | 135 | (if (= 1 (count params)) 136 | (first params) 137 | (let [templ (->> (repeat (count params) "~{}") 138 | (interpose "+") 139 | (reduce c/str ""))] 140 | (apply list 'js* templ params))))) 141 | 142 | (defn- compile-join-classes 143 | "Joins strings space separated" 144 | ([] "") 145 | ([x] x) 146 | ([x & xs] 147 | (compile-concat (interpose " " (cons x xs)) :safe? true))) 148 | 149 | (defn- compile-class-attr-value 150 | [value] 151 | (cond 152 | (or (nil? value) 153 | (keyword? value) 154 | (string? value)) 155 | value 156 | 157 | ;; If we know all classes at compile time, we just join them 158 | ;; correctly and return. 159 | (and (or (sequential? value) 160 | (set? value)) 161 | (every? string? value)) 162 | (join-classes value) 163 | 164 | ;; If we don't know all classes at compile time (some classes are 165 | ;; defined on let bindings per example), then we emit a efficient 166 | ;; concatenation code that executes on runtime 167 | (vector? value) 168 | (apply compile-join-classes value) 169 | 170 | :else value)) 171 | 172 | (defmulti compile-form 173 | "Pre-compile certain standard forms, where possible." 174 | (fn [form] 175 | (when (and (seq? form) (symbol? (first form))) 176 | (name (first form))))) 177 | 178 | (defmethod compile-form "do" 179 | [[_ & forms]] 180 | `(do ~@(butlast forms) ~(compile* (last forms)))) 181 | 182 | (defmethod compile-form "array" 183 | [[_ & forms]] 184 | `(cljs.core/array ~@(mapv compile* forms))) 185 | 186 | (defmethod compile-form "let" 187 | [[_ bindings & body]] 188 | `(let ~bindings ~@(butlast body) ~(compile* (last body)))) 189 | 190 | (defmethod compile-form "let*" 191 | [[_ bindings & body]] 192 | `(let* ~bindings ~@(butlast body) ~(compile* (last body)))) 193 | 194 | (defmethod compile-form "letfn*" 195 | [[_ bindings & body]] 196 | `(letfn* ~bindings ~@(butlast body) ~(compile* (last body)))) 197 | 198 | (defmethod compile-form "for" 199 | [[_ bindings body]] 200 | ;; Special optimization: For a simple (for [x xs] ...) we rewrite the for 201 | ;; to a fast reduce outputting a JS array: 202 | (if (== 2 (count bindings)) 203 | (let [[item coll] bindings] 204 | (if (= 'js (:tag (meta coll))) 205 | `(.map ~coll (fn [~item] ~(compile* body))) 206 | `(reduce (fn [out-arr# ~item] 207 | (.push out-arr# ~(compile* body)) 208 | out-arr#) 209 | (cljs.core/array) ~coll))) 210 | ;; Still optimize a little by giving React an array: 211 | (list 'cljs.core/into-array `(for ~bindings ~(compile* body))))) 212 | 213 | (defmethod compile-form "if" 214 | [[_ condition & body]] 215 | `(if ~condition ~@(doall (for [x body] (compile* x))))) 216 | 217 | (defmethod compile-form "when" 218 | [[_ bindings & body]] 219 | `(when ~bindings ~@(doall (for [x body] (compile* x))))) 220 | 221 | (defmethod compile-form "when-some" 222 | [[_ bindings & body]] 223 | `(when-some ~bindings ~@(butlast body) ~(compile* (last body)))) 224 | 225 | (defmethod compile-form "when-let" 226 | [[_ bindings & body]] 227 | `(when-let ~bindings ~@(butlast body) ~(compile* (last body)))) 228 | 229 | (defmethod compile-form "when-first" 230 | [[_ bindings & body]] 231 | `(when-first ~bindings ~@(butlast body) ~(compile* (last body)))) 232 | 233 | (defmethod compile-form "when-not" 234 | [[_ bindings & body]] 235 | `(when-not ~bindings ~@(doall (for [x body] (compile* x))))) 236 | 237 | (defmethod compile-form "if-not" 238 | [[_ bindings & body]] 239 | `(if-not ~bindings ~@(doall (for [x body] (compile* x))))) 240 | 241 | (defmethod compile-form "if-some" 242 | [[_ bindings & body]] 243 | `(if-some ~bindings ~@(doall (for [x body] (compile* x))))) 244 | 245 | (defmethod compile-form "if-let" 246 | [[_ bindings & body]] 247 | `(if-let ~bindings ~@(doall (for [x body] (compile* x))))) 248 | 249 | (defmethod compile-form "letfn" 250 | [[_ bindings & body]] 251 | `(letfn ~bindings ~@(butlast body) ~(compile* (last body)))) 252 | 253 | (defmethod compile-form "fn" 254 | [[_ params & body]] 255 | `(fn ~params ~@(butlast body) ~(compile* (last body)))) 256 | 257 | (defmethod compile-form "case" 258 | [[_ v & cases]] 259 | `(case ~v 260 | ~@(doall (mapcat 261 | (fn [[test hiccup]] 262 | (if hiccup 263 | [test (compile* hiccup)] 264 | [(compile* test)])) 265 | (partition-all 2 cases))))) 266 | 267 | (defmethod compile-form "condp" 268 | [[_ f v & cases]] 269 | `(condp ~f ~v 270 | ~@(doall (mapcat 271 | (fn [[test hiccup]] 272 | (if hiccup 273 | [test (compile* hiccup)] 274 | [(compile* test)])) 275 | (partition-all 2 cases))))) 276 | 277 | (defmethod compile-form "cond" 278 | [[_ & clauses]] 279 | `(cond ~@(doall 280 | (mapcat 281 | (fn [[check expr]] [check (compile* expr)]) 282 | (partition 2 clauses))))) 283 | 284 | (defmethod compile-form :default [expr] expr) 285 | 286 | (defn- compile-element 287 | "Returns an unevaluated form that will render the supplied vector as a HTML element." 288 | [[tag props & children :as element]] 289 | (cond 290 | ;; e.g. [:> Component {:key "xyz", :foo "bar} ch0 ch1] 291 | (contains? *handlers* tag) 292 | (let [f (get *handlers* tag) 293 | [tag props children] (apply f element)] 294 | (emit-jsx tag props (mapv compile* children))) 295 | 296 | ;; e.g. [:span {} x] 297 | (and (literal? tag) (map? props)) 298 | (let [[tag props _] (norm/element [tag props])] 299 | (emit-jsx tag props (mapv compile* children))) 300 | 301 | ;; We could now interpet this as either: 302 | ;; 1. First argument is the attributes (in #js{} provided by the user) OR: 303 | ;; 2. First argument is the first child element. 304 | ;; We assume #2. Always! 305 | (literal? tag) 306 | (compile-element (list* tag {} props children)) 307 | 308 | ;; Problem: [a b c] could be interpreted as: 309 | ;; 1. The coll of ReactNodes [a b c] OR 310 | ;; 2. a is a React Element, b are the props and c is the first child 311 | ;; We default to 1) (handled below) BUT, if b is a map, we know this must be 2) 312 | ;; since a map doesn't make any sense as a ReactNode. 313 | ;; [foo {...} ch0 ch1] NEVER makes sense to interpret as a sequence 314 | (and (vector? element) (map? props)) 315 | (emit-jsx tag props (mapv compile* children)) 316 | 317 | (seq? element) 318 | (seq (mapv compile* element)) 319 | 320 | ;; We have nested children 321 | ;; [[:div "foo"] [:span "foo"]] 322 | :else 323 | (mapv compile* element))) 324 | 325 | (defn- compile* 326 | "Pre-compile data structures" 327 | [content] 328 | (cond 329 | (vector? content) (compile-element content) 330 | (literal? content) content 331 | :else (compile-form content))) 332 | 333 | (defn compile-prop-key 334 | "Compiles a key to a react compatible key (eg: camelCase)" 335 | [k] 336 | (if (or (keyword? k) (symbol? k)) 337 | (util/ident->prop k) 338 | k)) 339 | 340 | (defn compile-prop-inner-key 341 | "Compiles a key to a react compatible key (eg: camelCase)" 342 | [k] 343 | (if (or (keyword? k) (symbol? k)) 344 | (util/ident->key k) 345 | k)) 346 | 347 | (defn- compile-style-value 348 | [m] 349 | (cond 350 | (map? m) 351 | (reduce-kv 352 | (fn [m k v] 353 | (assoc m (compile-prop-key k) v)) 354 | {} m) 355 | ;; React native accepts :style [{:foo-bar ..} other-styles] so camcase those keys: 356 | (vector? m) 357 | (mapv compile-style-value m) 358 | 359 | :else 360 | m)) 361 | 362 | (defn compile-prop-value 363 | [level val] 364 | (cond 365 | (not *transform-props-recursive*) 366 | val 367 | 368 | (map? val) 369 | (->> val 370 | (into {} (map (partial compile-prop (inc level)))) 371 | (compile-map-to-js)) 372 | 373 | (vector? val) 374 | (->> val 375 | (mapv (partial compile-prop-value (inc level))) 376 | (compile-vec-to-js)) 377 | 378 | :else 379 | val)) 380 | 381 | (defn compile-prop 382 | ([prop] (compile-prop 1 prop)) 383 | ([level [key val :as kvpair]] 384 | (let [key (if (= level 1) 385 | (compile-prop-key key) 386 | (compile-prop-inner-key key))] 387 | (cond 388 | (and (= level 1) 389 | (= key "className")) 390 | [key (compile-class-attr-value val)] 391 | 392 | (and (= level 1) 393 | (= key "style")) 394 | [key (-> val 395 | (compile-style-value) 396 | (compile-map-to-js))] 397 | 398 | (and (= level 1) 399 | (= key "htmlFor")) 400 | [key (if (keyword? val) 401 | (name val) 402 | val)] 403 | 404 | :else 405 | [key (compile-prop-value level val)])))) 406 | 407 | (defn compile-kv-to-js 408 | "A internal method helper for compile kv data structures" 409 | [form] 410 | (let [valid-key? #(or (keyword? %) (string? %)) 411 | form (into {} (filter (comp valid-key? key)) form)] 412 | [(->> form 413 | (map (comp name key)) 414 | (map #(-> (str \' % "':~{}"))) 415 | (interpose ",") 416 | (apply str)) 417 | (vec (vals form))])) 418 | 419 | (defn compile-map-to-js 420 | "Compile a statically known map data sturcture, non-recursivelly to js 421 | expression. Mainly used by macros for create js data structures at 422 | compile time." 423 | [form] 424 | (if (map? form) 425 | (if (empty? form) 426 | (list 'js* "{}") 427 | (let [[keys vals] (compile-kv-to-js form)] 428 | (-> (apply list 'js* (str "{" keys "}") vals) 429 | (vary-meta assoc :tag 'object)))) 430 | form)) 431 | 432 | (defn compile-vec-to-js 433 | "Compile a statically known map data sturcture, non-recursivelly to js 434 | expression. Mainly used by macros for create js data structures at 435 | compile time." 436 | [form] 437 | (if (vector? form) 438 | (if (empty? form) 439 | (list 'js* "[]") 440 | (let [template (->> form 441 | (map (constantly "~{}")) 442 | (interpose ",") 443 | (apply str))] 444 | (-> (apply list 'js* (str "[" template "]") form) 445 | (vary-meta assoc :tag 'object)))) 446 | form)) 447 | 448 | (defn compile-props-to-js 449 | "Transform a props map literal to js object props. By default not 450 | recursive." 451 | [props & {:keys [::transform-props-recursive 452 | ::transform-props-keys] 453 | :or {transform-props-recursive false 454 | transform-props-keys true} 455 | :as params}] 456 | 457 | (binding [*transform-props-recursive* transform-props-recursive] 458 | (cond->> props 459 | (true? transform-props-keys) 460 | (into {} (map (partial compile-prop 1))) 461 | 462 | :always 463 | (compile-map-to-js)))) 464 | 465 | (defn compile-coll-to-js 466 | "Transform map or vector to js object or js array. Recursive by 467 | default." 468 | [coll & {:keys [::transform-props-recursive 469 | ::transform-props-keys] 470 | :or {transform-props-recursive true 471 | transform-props-keys true} 472 | :as params}] 473 | (binding [*transform-props-recursive* transform-props-recursive] 474 | (cond 475 | (map? coll) 476 | (->> coll 477 | (into {} (map (partial compile-prop 2))) 478 | (compile-map-to-js)) 479 | 480 | (vector? coll) 481 | (->> coll 482 | (mapv (partial compile-prop-value 2)) 483 | (compile-vec-to-js)) 484 | 485 | :else 486 | (throw (ex-info "only map or vectors allowed" {}))))) 487 | 488 | (defn compile-to-js-spread 489 | [target other compile-prop] 490 | (cond 491 | (and (symbol? target) 492 | (symbol? other)) 493 | (list 'js* "{...~{}, ...~{}}" target other) 494 | 495 | (and (symbol? target) 496 | (map? other)) 497 | (let [[keys vals] (->> other 498 | (into {} (map compile-prop)) 499 | (compile-kv-to-js)) 500 | template (str "{...~{}, " keys "}")] 501 | (apply list 'js* template target vals)) 502 | 503 | (and (map? target) 504 | (symbol? other)) 505 | (let [[keys vals] (->> target 506 | (into {} (map compile-prop)) 507 | (compile-kv-to-js)) 508 | template (str "{" keys ", ...~{}}")] 509 | (apply list 'js* template (concat vals [other]))) 510 | 511 | (and (map? target) 512 | (map? other)) 513 | (compile-map-to-js (->> (merge target other) 514 | (into {} (map compile-prop)))) 515 | 516 | :else 517 | (throw (IllegalArgumentException. "invalid arguments, only symbols or maps allowed")))) 518 | 519 | (defn emit-jsx 520 | "Emits the final react js code" 521 | [tag props children] 522 | (let [tag (cond 523 | (keyword? tag) (name tag) 524 | (string? tag) tag 525 | (symbol? tag) tag 526 | (seq? tag) tag 527 | :else (throw (ex-info "jsx: invalid tag" {:tag tag}))) 528 | 529 | children (into [] (filter some?) children) 530 | mdata (meta props) 531 | jstag? (= (get mdata :tag) 'js)] 532 | 533 | (if (valid-props-type? props) 534 | (if (or (map? props) (nil? props)) 535 | (let [nchild (count children) 536 | props (cond 537 | (= 0 nchild) 538 | (or props {}) 539 | 540 | (= 1 nchild) 541 | (assoc props :children (peek children)) 542 | 543 | :else 544 | (assoc props :children (apply list 'cljs.core/array children))) 545 | 546 | key (:key props) 547 | props (dissoc props :key) 548 | props (compile-props-to-js props mdata)] 549 | 550 | (if key 551 | (if (> nchild 1) 552 | (list 'rumext.v2/jsxs tag props key) 553 | (list 'rumext.v2/jsx tag props key)) 554 | (if (> nchild 1) 555 | (list 'rumext.v2/jsxs tag props) 556 | (list 'rumext.v2/jsx tag props)))) 557 | 558 | (let [props (if (and (::allow-dynamic-transform mdata) (not jstag?)) 559 | (list 'rumext.v2.util/map->obj props) 560 | props) 561 | nchild (count children)] 562 | (cond 563 | (= 0 nchild) 564 | (list 'rumext.v2/create-element tag props) 565 | 566 | (= 1 nchild) 567 | (list 'rumext.v2/create-element tag props (first children)) 568 | 569 | :else 570 | (apply list 'rumext.v2/create-element tag props children)))) 571 | 572 | (throw (ex-info "jsx: invalid props type" {:props props}))))) 573 | 574 | (defn compile 575 | "Arguments: 576 | - content: The hiccup to compile 577 | - handlers: A map to handle special tags. See default-handlers in this namespace. 578 | " 579 | ([content] 580 | (compile content nil)) 581 | ([content handlers] 582 | (binding [*handlers* (merge default-handlers handlers)] 583 | (compile* content)))) 584 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rumext 2 | 3 | Simple and Decomplected UI library based on React >= 18 focused on performance. 4 | 5 | ## Installation 6 | 7 | Add to `deps.edn`: 8 | 9 | ```clojure 10 | funcool/rumext 11 | {:git/tag "v2.21" 12 | :git/sha "072d671" 13 | :git/url "https://github.com/funcool/rumext.git"} 14 | ``` 15 | 16 | ## User Guide 17 | 18 | Rumext is a tool to build a web UI in ClojureScript. 19 | 20 | It's a thin wrapper on [React](https://react.dev/) >= 18, focused on 21 | performance and offering a Clojure-idiomatic interface. 22 | 23 | **API Reference**: http://funcool.github.io/rumext/latest/ 24 | 25 | 26 | It uses Clojure macros to achieve the same goal as [JSX 27 | format](https://react.dev/learn/writing-markup-with-jsx) without using anything 28 | but the plain Clojure syntax. The HTML is expressed in a format inspired 29 | in [hiccup library](https://github.com/weavejester/hiccup), but with its own 30 | implementation. 31 | 32 | HTML code is represented as nested arrays with keywords for tags and 33 | attributes. Example: 34 | 35 | ```clojure 36 | [:div {:class "foobar" 37 | :style {:background-color "red"} 38 | :on-click some-on-click-fn} 39 | "Hello World"] 40 | ``` 41 | 42 | Macros are smart enough to transform attribute names from `lisp-case` 43 | to `camelCase` and renaming `:class` to `className`. So the compiled javacript 44 | code for this fragment could be something like: 45 | 46 | ```js 47 | React.createElement("div", 48 | {className: "foobar", 49 | style: {"backgroundColor": "red"}, 50 | onClick: someOnClickFn}, 51 | "Hello World"); 52 | ``` 53 | 54 | And this is what will be rendered when the app is loaded in a browser: 55 | 56 | ```html 57 |
60 | Hello World 61 |
62 | ``` 63 | 64 | **WARNING**: it is mainly implemented to be used in 65 | [Penpot](https://github.com/penpot/penpot) and released as separated project 66 | for conveniendce. Don't expect compromise for backwards compatibility beyond 67 | what the penpot project needs. 68 | 69 | ### Instantiating elements and custom components 70 | 71 | #### Passing props 72 | 73 | As seen above, when using the [Hiccup-like](https://github.com/weavejester/hiccup) 74 | syntax, you can create a HTML element with a keyword like `:div`, `:span` or 75 | `:p`. You can also specify a map of attributes, that are converted at compile 76 | time into a Javascript object. 77 | 78 | **IMPORTANT**: a Javascript plain object is different from a Clojure plain map. 79 | In ClojureScript you can handle mutable JS objects with a specific API, and 80 | convert forth and back to Clojure maps. You can learn more about it in 81 | [ClojureScript Unraveled](https://funcool.github.io/clojurescript-unraveled/#javascript-objects) 82 | book. 83 | 84 | Rumext macros have some features to pass properties in a more convenient and 85 | Clojure idiomatic way. For example, when using the `[:div {...}]` syntax, you 86 | do not need to add the `#js` prefix, it's added automatically. There are also 87 | some automatic transformations of property names: 88 | 89 | * Names in `lisp-case` are transformed to `camelCase`. 90 | * Reserved names like `class` are transformed to React convention, like 91 | `className`. 92 | * Names already in `camelCase` are passed directly without transform. 93 | * Properties that begin with `data-` and `aria-` are also passed directly. 94 | * Transforms are applied only to `:keyword` properties. You can also send 95 | string properties, that are not processed anyway. 96 | 97 | It's important to notice that this transformations are performed at compile time, 98 | having no impact in runtime performance. 99 | 100 | 101 | #### Dynamic element names and attributes 102 | 103 | There are times when we'll need the element name to be chosen dynamically or 104 | constructed at runtime; the props to be built dynamically or created as an 105 | element from a user-defined component. 106 | 107 | For this purpose, Rumext exposes a special macro: `:>`, a general-purpose 108 | handler for passing dynamically defined props to DOM native elements or 109 | creating elements from user-defined components. 110 | 111 | To define the element dynamically, just pass a variable with the name as a 112 | first parameter of `:>`. 113 | 114 | ```clojure 115 | (let [element (if something "div" "span")] 116 | [:> element {:class "foobar" 117 | :style {:background-color "red"} 118 | :on-click some-on-click-fn} 119 | "Hello World"]) 120 | ``` 121 | 122 | To give a dynamic map of properties, you may also give a variable as a 123 | second parameter: 124 | 125 | ```clojure 126 | (let [props #js {:className "fooBar" 127 | :style #js {:backgroundColor "red"} 128 | :onClick some-on-click}] 129 | [:> "div" props 130 | "Hello World"]) 131 | ``` 132 | 133 | **IMPORTANT** if you define the attributes dynamically, outside the `:>` macro, 134 | there are no automatic transformations. So you need to define the map as a 135 | plain Javascript object with the `#js` prefix or any other way. You also need 136 | to use `camelCase` names and remember to use `className` instead of `class`, 137 | for example. 138 | 139 | There are a couple of utilities for managing dynamic attributes in a more 140 | convenient way. 141 | 142 | 143 | ##### `mf/spread-props` 144 | 145 | Or shorter alias: `mf/spread` 146 | 147 | A macro that allows performing a merge between two props data structures using 148 | the JS spread operator (`{...props1, ...props2}`). This macro also performs 149 | name transformations if you pass a literal map as a second parameter. 150 | 151 | It is commonly used this way: 152 | 153 | ```clojure 154 | (mf/defc my-label* 155 | [{:keys [name class on-click] :rest props}] 156 | (let [class (or class "my-label") 157 | props (mf/spread-props props {:class class})] 158 | [:span {:on-click on-click} 159 | [:> :label props name]])) 160 | ``` 161 | 162 | Very similar to `mf/spread-props` but without react flavored props 163 | transformations you have the `mf/spread-object`. 164 | 165 | In both cases, if both arguments are symbols, no transformation 166 | can be applied because is unknown the structure at compile time. 167 | 168 | 169 | ##### `mf/props` 170 | 171 | A helper macro to create a Javascript props object from a Clojure map, 172 | applying name transformations. 173 | 174 | An example of how it can be used and combined with `mf/spread-props`: 175 | 176 | ```clojure 177 | (mf/defc my-label* 178 | [{:keys [name class on-click] :rest props}] 179 | (let [class (or class "my-label") 180 | new-props (mf/props {:class class}) 181 | all-props (mf/spread-props props new-props)] 182 | [:span {:on-click on-click} 183 | [:> :label props name]])) 184 | ``` 185 | 186 | 187 | ##### `mf/object` 188 | 189 | A helper macro for create javascript objects from clojure literals. It works recursiverlly. 190 | 191 | ```clojure 192 | (mf/object {:a [1 2 3]}) 193 | 194 | ;; Is analogous to 195 | #js {:a #js [1 2 3]} 196 | ``` 197 | 198 | 199 | ##### `mfu/map->props` 200 | 201 | In some cases you will need to make props from a dynamic Clojure 202 | object. You can use `mf/map->props` function for it, but be aware that 203 | it makes the conversion to Javascript and the names transformations in 204 | runtime, so it adds some overhead in each render. Consider not using 205 | it if performance is important. 206 | 207 | ```clojure 208 | (require '[rumext.v2.utils :as mfu]) 209 | 210 | (let [clj-props {:class "my-label"} 211 | props (mfu/map->props clj-props)] 212 | [:> :label props name]) 213 | ``` 214 | 215 | ##### `mfu/bean` 216 | 217 | A helper that allows create a proxy object from javascript object that 218 | has the same semantics as clojure map and clojure vectors. Allows 219 | handle clojure and javascript parameters in a transparent way. 220 | 221 | ```clojure 222 | (require '[rumext.v2.utils :as mfu]) 223 | 224 | (mf/defc my-select* 225 | [{:keys [options] :rest props}] 226 | (let [options (mfu/bean options) 227 | ;; from here, options looks like a clojure vector 228 | ;; independently if it passed as clojure vector 229 | ;; or js array. 230 | ] 231 | [:select ...])) 232 | ``` 233 | 234 | #### Instantiating a custom component 235 | 236 | You can pass to `:>` macro the name of a custom component (see [below](#creating-a-react-custom-component)) 237 | to create an instance of it: 238 | 239 | ```clojure 240 | (mf/defc my-label* 241 | [{:keys [name class on-click] :rest props}] 242 | [:span {:on-click on-click} 243 | [:> :label props name]]) 244 | 245 | (mf/defc other-component* 246 | [] 247 | [:> my-label* {:name "foobar" :on-click some-fn}]) 248 | ``` 249 | 250 | ### Creating a React custom component 251 | 252 | The `defc` macro is the basic block of a Rumext UI. It's a lightweight utility 253 | that generates a React **function component** and adds some adaptations for it 254 | to be more convenient to ClojureScript code, like `camelCase` conversions and 255 | reserved name changes as explained [above](#passing-props). 256 | 257 | For example, this defines a React component: 258 | 259 | ```clojure 260 | (require '[rumext.v2 :as mf]) 261 | 262 | (mf/defc title* 263 | [{:keys [label-text] :as props}] 264 | [:div {:class "title"} label-text]) 265 | ``` 266 | 267 | The compiled javascript for this block will be similar to what would be 268 | obtained for this JSX block: 269 | 270 | ```js 271 | function title({labelText}) { 272 | return ( 273 |
274 | {labelText} 275 |
276 | ); 277 | } 278 | ``` 279 | 280 | **NOTE**: the `*` in the component name is a mandatory convention for proper 281 | visual distinction of React components and Clojure functions. It also enables 282 | the current defaults on how props are handled. If you don't use the `*` suffix, 283 | the component will behave in legacy mode (see the [FAQs](#faq) below). 284 | 285 | The component created this way can be mounted onto the DOM: 286 | 287 | ```clojure 288 | (ns myname.space 289 | (:require 290 | [goog.dom :as dom] 291 | [rumext.v2 :as mf])) 292 | 293 | (def root (mf/create-root (dom/getElement "app"))) 294 | (mf/render! root (mf/html [:> title* {:label-text "hello world"}])) 295 | ``` 296 | 297 | Or you can use `mf/element`, but in this case you need to give the 298 | attributes in the raw Javascript form, because this macro does not have 299 | automatic conversions: 300 | 301 | ```clojure 302 | (ns myname.space 303 | (:require 304 | [goog.dom :as dom] 305 | [rumext.v2 :as mf])) 306 | 307 | (def root (mf/create-root (dom/getElement "app"))) 308 | (mf/render! root (mf/element title* #js {:labelText "hello world"})) 309 | ``` 310 | 311 | ### Reading component props & destructuring 312 | 313 | When React instantiates a function component, it passes a `props` parameter 314 | that is a map of the names and values of the attributes defined in the calling 315 | point. 316 | 317 | Normally, Javascript objects cannot be destructured. But the `defc` macro 318 | implements a destructuring functionality, that is similar to what you can do 319 | with Clojure maps, but with small differences and convenient enhancements for 320 | making working with React props and idioms easy, like `camelCase` conversions 321 | as explained [above](#passing-props). 322 | 323 | ```clojure 324 | (mf/defc title* 325 | [{:keys [title-name] :as props}] 326 | (assert (object? props) "expected object") 327 | (assert (string? title-name) "expected string") 328 | [:label {:class "label"} title-name]) 329 | ``` 330 | 331 | If the component is called via the `[:>` macro (explained [above](#dynamic-element-names-and-attributes)), 332 | there will be two compile-time conversion, one when calling and another one when 333 | destructuring. In the Clojure code all names will be `lisp-case`, but if you 334 | inspect the generated Javascript code, you will see names in `camelCase`. 335 | 336 | #### Default values 337 | 338 | Also like usual destructuring, you can give default values to properties by 339 | using the `:or` construct: 340 | 341 | ```clojure 342 | (mf/defc color-input* 343 | [{:keys [value select-on-focus] :or {select-on-focus true} :as props}] 344 | ...) 345 | ``` 346 | 347 | #### Rest props 348 | 349 | An additional idiom (specific to the Rumext component macro and not available 350 | in standard Clojure destructuring) is the ability to obtain an object with all 351 | non-destructured props with the `:rest` construct. This allows to extract the 352 | props that the component has control of and leave the rest in an object that 353 | can be passed as-is to the next element. 354 | 355 | ```clojure 356 | (mf/defc title* 357 | [{:keys [name] :rest props}] 358 | (assert (object? props) "expected object") 359 | (assert (nil? (unchecked-get props "name")) "no name in props") 360 | 361 | ;; See below for the meaning of `:>` 362 | [:> :label props name]) 363 | ``` 364 | 365 | #### Reading props without destructuring 366 | 367 | Of course the destructure is optional. You can receive the complete `props` 368 | argument and read the properties later. But in this case you will not have 369 | the automatic conversions: 370 | 371 | ```clojure 372 | (mf/defc color-input* 373 | [props] 374 | (let [value (unchecked-get props "value") 375 | on-change (unchecked-get props "onChange") 376 | on-blur (unchecked-get props "onBlur") 377 | on-focus (unchecked-get props "onFocus") 378 | select-on-focus? (or (unchecked-get props "selectOnFocus") true) 379 | class (or (unchecked-get props "className") "color-input") 380 | ``` 381 | 382 | The recommended way of reading `props` javascript objects is by using the 383 | Clojurescript core function `unchecked-get`. This is directly translated to 384 | Javascript `props["propName"]`. As Rumext is performance oriented, this is the 385 | most efficient way of reading props for the general case. Other methods like 386 | `obj/get` in Google Closure Library add extra safety checks, but in this case 387 | it's not necessary since the `props` attribute is guaranteed by React to have a 388 | value, although it can be an empty object. 389 | 390 | #### Forwarding references 391 | 392 | In React there is a mechanism to set a reference to the rendered DOM element, if 393 | you need to manipulate it later. Also it's possible that a component may receive 394 | this reference and gives it to a inner element. This is called "forward referencing" 395 | and to do it in Rumext, you need to add the `forward-ref` metadata. Then, the 396 | reference will come in a second argument to the `defc` macro: 397 | 398 | ```clojure 399 | (mf/defc wrapped-input* 400 | {::mf/forward-ref true} 401 | [props ref] 402 | (let [...] 403 | [:input {:style {...} 404 | :ref ref 405 | ...}])) 406 | ``` 407 | 408 | In React 19 this will not be necessary, since you will be able to pass the ref 409 | directly inside `props`. But Rumext currently only support React 18. 410 | 411 | ### Props Checking 412 | 413 | The Rumext library comes with two approaches for checking props: 414 | **simple** and **malli**. 415 | 416 | Let's start with the **simple**, which consists of simple existence checks or 417 | plain predicate checking. For this, we have the `mf/expect` macro that receives 418 | a Clojure set and throws an exception if any of the props in the set has not 419 | been given to the component: 420 | 421 | ```clojure 422 | (mf/defc button* 423 | {::mf/expect #{:name :on-click}} 424 | [{:keys [name on-click]}] 425 | [:button {:on-click on-click} name]) 426 | ``` 427 | 428 | The prop names obey the same rules as the destructuring so you should use the 429 | same names. 430 | 431 | Sometimes a simple existence check is not enough; for those cases, you can give 432 | `mf/expect` a map where keys are props and values are predicates: 433 | 434 | ```clojure 435 | (mf/defc button* 436 | {::mf/expect {:name string? 437 | :on-click fn?}} 438 | [{:keys [name on-click]}] 439 | [:button {:on-click on-click} name]) 440 | ``` 441 | 442 | If that is not enough, you can use `mf/schema` macro that supports 443 | **[malli](https://github.com/metosin/malli)** schemas as a validation 444 | mechanism for props: 445 | 446 | ```clojure 447 | (def ^:private schema:props 448 | [:map {:title "button:props"} 449 | [:name string?] 450 | [:class {:optional true} string?] 451 | [:on-click fn?]]) 452 | 453 | (mf/defc button* 454 | {::mf/schema schema:props} 455 | [{:keys [name on-click]}] 456 | [:button {:on-click on-click} name]) 457 | ``` 458 | 459 | **IMPORTANT**: The props checking obeys the `:elide-asserts` compiler 460 | option and by default, they will be removed in production builds if 461 | the configuration value is not changed explicitly. 462 | 463 | ### Hooks 464 | 465 | You can use React hooks as is, as they are exposed by Rumext as 466 | `mf/xxx` wrapper functions. Additionaly, Rumext offers several 467 | specific hooks that adapt React ones to have a more Clojure idiomatic 468 | interface. 469 | 470 | You can use both one and the other interchangeably, depending on which 471 | type of API you feel most comfortable with. The React hooks are exposed 472 | as they are in React, with the function name in `camelCase`, and the 473 | Rumext hooks use the `lisp-case` syntax. 474 | 475 | Only a subset of available hooks is documented here; please refer to 476 | the [React API reference 477 | documentation](https://react.dev/reference/react/hooks) for detailed 478 | information about available hooks. 479 | 480 | #### `use-state` 481 | 482 | This is analogous to the `React.useState`. It offers the same 483 | functionality but uses the ClojureScript atom interface. 484 | 485 | Calling `mf/use-state` returns an atom-like object that will deref to 486 | the current value, and you can call `swap!` and `reset!` on it to 487 | modify its state. The returned object always has a stable reference 488 | (no changes between rerenders). 489 | 490 | Any mutation will schedule the component to be rerendered. 491 | 492 | ```clojure 493 | (require '[rumext.v2 as mf]) 494 | 495 | (mf/defc local-state* 496 | [props] 497 | (let [clicks (mf/use-state 0)] 498 | [:div {:on-click #(swap! clicks inc)} 499 | [:span "Clicks: " @clicks]])) 500 | ``` 501 | 502 | This is functionally equivalent to using the React hook directly: 503 | 504 | ```clojure 505 | (mf/defc local-state* 506 | [props] 507 | (let [[counter update-counter] (mf/useState 0)] 508 | [:div {:on-click (partial update-counter #(inc %))} 509 | [:span "Clicks: " counter]])) 510 | ``` 511 | 512 | #### `use-var` 513 | 514 | In the same way as `use-state` returns an atom-like object. The unique 515 | difference is that updating the ref value does not schedule the 516 | component to rerender. Under the hood, it uses the `useRef` hook. 517 | 518 | **DEPRECATED:** should not be used 519 | 520 | #### `use-effect` 521 | 522 | Analogous to the `React.useEffect` hook with a minimal call convention 523 | change (the order of arguments is inverted). 524 | 525 | This is a primitive that allows incorporating probably effectful code 526 | into a functional component: 527 | 528 | ```clojure 529 | (mf/defc local-timer* 530 | [props] 531 | (let [local (mf/use-state 0)] 532 | (mf/use-effect 533 | (fn [] 534 | (let [sem (js/setInterval #(swap! local inc) 1000)] 535 | #(js/clearInterval sem)))) 536 | [:div "Counter: " @local])) 537 | ``` 538 | 539 | The `use-effect` is a two-arity function. If you pass a single 540 | callback function, it acts as though there are no dependencies, so the 541 | callback will be executed once per component (analogous to `didMount` 542 | and `willUnmount`). 543 | 544 | If you want to pass dependencies, you have two ways: 545 | 546 | - passing a JS array as a first argument (like in React but with 547 | inverted order). 548 | - using the `rumext.v2/deps` helper: 549 | 550 | ```clojure 551 | (mf/use-effect 552 | (mf/deps x y) 553 | (fn [] (do-stuff x y))) 554 | ``` 555 | 556 | And finally, if you want to execute it on each render, pass `nil` as 557 | deps (much in the same way as raw `useEffect` works). 558 | 559 | For convenience, there is an `mf/with-effect` macro that drops one 560 | level of indentation: 561 | 562 | ```clojure 563 | (mf/defc local-timer* 564 | [props] 565 | (let [local (mf/use-state 0)] 566 | (mf/with-effect [] 567 | (let [sem (js/setInterval #(swap! local inc) 1000)] 568 | #(js/clearInterval sem))) 569 | [:div "Counter: " @local])) 570 | ``` 571 | 572 | Here, the deps must be passed as elements within the vector (the first 573 | argument). 574 | 575 | Obviously, you can also use the React hook directly via `mf/useEffect`. 576 | 577 | #### `use-memo` 578 | 579 | In the same line as the `use-effect`, this hook is analogous to the 580 | React `useMemo` hook with the order of arguments inverted. 581 | 582 | The purpose of this hook is to return a memoized value. 583 | 584 | Example: 585 | 586 | ```clojure 587 | (mf/defc sample-component* 588 | [{:keys [x]}] 589 | (let [v (mf/use-memo (mf/deps x) #(pow x 10))] 590 | [:span "Value is: " v])) 591 | ``` 592 | 593 | On each render, while `x` has the same value, the `v` only will be 594 | calculated once. 595 | 596 | This also can be expressed with the `rumext.v2/with-memo` macro that 597 | removes a level of indentation: 598 | 599 | ```clojure 600 | (mf/defc sample-component* 601 | [{:keys [x]}] 602 | (let [v (mf/with-memo [x] 603 | (pow x 10))] 604 | [:span "Value is: " v])) 605 | ``` 606 | 607 | #### `use-fn` 608 | 609 | Is a special case of `use-memo`in that the memoized value is a 610 | function definition. 611 | 612 | An alias for `use-callback`, that is a wrapper on `React.useCallback`. 613 | 614 | #### `deref` 615 | 616 | A Rumext custom hook that adds reactivity to atom changes to the 617 | component. Calling `mf/deref` returns the same value as the Clojure 618 | `deref`, but also sets a component rerender when the value changes. 619 | 620 | Example: 621 | 622 | ```clojure 623 | (def clock (atom (.getTime (js/Date.)))) 624 | (js/setInterval #(reset! clock (.getTime (js/Date.))) 160) 625 | 626 | (mf/defc timer* 627 | [props] 628 | (let [ts (mf/deref clock)] 629 | [:div "Timer (deref): " 630 | [:span ts]])) 631 | ``` 632 | 633 | Internally, it uses the `react.useSyncExternalStore` API together with 634 | the ability of atom to watch it. 635 | 636 | ### Higher-Order Components 637 | 638 | React allows to create a component that adapts or wraps another component 639 | to extend it and add additional functionality. Rumext includes a convenient 640 | mechanism for doing it: the `::mf/wrap` metadata. 641 | 642 | Currently Rumext exposes one such component: 643 | 644 | - `mf/memo`: analogous to `React.memo`, adds memoization to the 645 | component based on props comparison. This allows to completely 646 | avoid execution to the component function if props have not changed. 647 | 648 | ```clojure 649 | (mf/defc title* 650 | {::mf/wrap [mf/memo]} 651 | [{:keys [name]}] 652 | [:div {:class "label"} name]) 653 | ``` 654 | 655 | By default, the `identical?` predicate is used to compare props; you 656 | can pass a custom comparator function as a second argument: 657 | 658 | ```clojure 659 | (mf/defc title* 660 | {::mf/wrap [#(mf/memo % =)]} 661 | [{:keys [name]}] 662 | [:div {:class "label"} name]) 663 | ``` 664 | 665 | For more convenience, Rumext has a special metadata `::mf/memo` that 666 | facilitates the general case for component props memoization. If you 667 | pass `true`, it will behave the same way as `::mf/wrap [mf/memo]` or 668 | `React.memo(Component)`. You also can pass a set of fields; in this 669 | case, it will create a specific function for testing the equality of 670 | that set of props. 671 | 672 | If you want to create your own higher-order component, you can use the 673 | `mf/fnc` macro: 674 | 675 | ```clojure 676 | (defn some-factory 677 | [component param] 678 | (mf/fnc my-high-order-component* 679 | [props] 680 | [:section 681 | [:> component props]])) 682 | ``` 683 | 684 | ### FAQ 685 | 686 | #### Differences with RUM 687 | 688 | This project was originated as a friendly fork of 689 | [rum](https://github.com/tonsky/rum) for a personal use but it later 690 | evolved to be a completly independent library that right now does not 691 | depend on it and probably no longer preserves any of the original 692 | code. In any case, many thanks to Tonksy for creating rum. 693 | 694 | This is the list of the main differences: 695 | 696 | - use function based components instead of class based components. 697 | - a clojurescript friendly abstractions for React Hooks. 698 | - the component body is compiled statically (never interprets at 699 | runtime thanks to **hicada**). 700 | - performance focused, with a goal to offer almost 0 runtime 701 | overhead on top of React. 702 | 703 | 704 | #### Why the import alias is `mf` in the examples? 705 | 706 | The usual convention of importing RUM project was to use `rum/defc` or 707 | `m/defc`. For Rumext the most straightforward abbreviation would have been 708 | `mx/defc`. But that preffix was already use for something else. So finally we 709 | choose `mf/defc`. But this is not mandatory, it's only a convention we follow 710 | in this manual and in Penpot. 711 | 712 | 713 | #### What is the legacy mode? 714 | 715 | In earlier versions of Rumext, components had a default behavior of 716 | automatically converting the `props` Javascript object coming from 717 | React to a Clojure object, so it could be read by normal destructuring 718 | or any other way of reading objects. 719 | 720 | Additionally you could use `:&` handler instead of `:>` to give a 721 | Clojure object that was converted into Javascript for passing it to 722 | React. 723 | 724 | But both kind of transformations were done in runtime, thus adding 725 | the conversion overhead to each render of the compoennt. Since Rumex 726 | is optimized for performance, this behavior is now deprecated. With 727 | the macro destructuring and other utilities explained above, you can 728 | do argument passing almost so conveniently, but with all changes done 729 | in compile time. 730 | 731 | Currently, components whose name does not use `*` as a suffix behave 732 | in legacy mode. You can activate the new behavior by adding the 733 | `::mf/props :obj` metadata, but all this is considered deprecated now. 734 | All new components should use `*` in the name. 735 | 736 | ## License 737 | 738 | Licensed under MPL-2.0 (see [LICENSE](LICENSE) file on the root of the repository) 739 | --------------------------------------------------------------------------------