├── .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 |
40 |
41 |
42 |
43 |
Timers
44 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
Reactive binary clock
56 |
57 |
58 |
59 |
60 |
Reactive artboard
61 |
62 |
63 |
64 |
68 |
69 |
73 |
74 |
78 |
79 |
80 |
BMI Calculator
81 |
82 |
83 |
84 |
85 |
Form validation
86 |
87 |
88 |
89 |
90 |
Self-reference
91 |
92 |
93 |
94 |
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 |
--------------------------------------------------------------------------------