2016
12 |The Blessing of Interactive Development 4/11 13 |
2015
14 |The Web After Tomorrow 6/23 15 |
A shallow dive into DataScript internals 2/23 16 |
2014
17 |Couple of DataScript resources 12/18 18 |
Streams: Mail 3.0 concept 10/27 19 |
Another powered-by-DataScript example 10/6 20 |
Chatting cats use DataScript for fun 9/18 21 |
Irrelevant Things 8/12 22 |
Reinventing Git interface 6/17 23 |
Unofficial guide to Datomic internals 5/6 24 |
Datomic as a Protocol 4/29 25 |
Decomposing web app development 4/24 26 | 27 | 28 | -------------------------------------------------------------------------------- /core/test/uix/aot_test.clj: -------------------------------------------------------------------------------- 1 | (ns uix.aot-test 2 | (:require [clojure.test :refer :all] 3 | [uix.compiler.aot :as aot] 4 | [cljs.env :as env] 5 | [cljs.analyzer :as ana] 6 | [cljs.compiler :as cljsc])) 7 | 8 | (deftest test-static-value? 9 | (is (aot/static-value? 1)) 10 | (is (aot/static-value? 'cljs.core.uix_hoisted_)) 11 | (is (= false (aot/static-value? 'x))) 12 | (is (aot/static-value? [1 :k "s"]))) 13 | 14 | (deftest test-static-element? 15 | (is (aot/static-element? :div {} [1 2 3])) 16 | (is (false? (aot/static-element? :div {:ref 1} [1 2 3])))) 17 | 18 | (deftest test-gen-constant-id 19 | (is (= 'uix-hoisted-0 (aot/gen-constant-id 0)))) 20 | 21 | (deftest test-register-constant! 22 | (binding [env/*compiler* (atom {})] 23 | (aot/register-constant! {:ns {:name 'cljs.core}} 0) 24 | (is (= @env/*compiler* 25 | '{::ana/constant-table {0 uix-hoisted-0}, 26 | ::ana/namespaces {cljs.core {::ana/constants {:seen #{0}, :order [0]}}}})))) 27 | 28 | (deftest test-maybe-hoist 29 | (is (= 'cljs.core.uix_hoisted__576654167 30 | (binding [env/*compiler* (atom {:options {:optimize-constants true}}) 31 | aot/*cljs-env* {}] 32 | (aot/maybe-hoist [:h1] "h1" :h1 {} [1])))) 33 | (is (= (binding [env/*compiler* (atom {:options {:optimize-constants false}}) 34 | aot/*cljs-env* {}] 35 | (aot/maybe-hoist [:h1] "h1" :h1 {} [1])) 36 | "h1"))) 37 | 38 | (deftest test-compile-html 39 | (is (= (binding [env/*compiler* (atom {})] 40 | (aot/compile-html [:h1] {})) 41 | '(js* 42 | "{'$$typeof':~{},'type':~{},'ref':~{},'key':~{},'props':~{},'_owner':~{}}" 43 | (js* "Symbol.for(~{})" "react.element") 44 | "h1" nil nil (js* "{}") nil))) 45 | (is (= (binding [env/*compiler* (atom {:options {:optimize-constants true}})] 46 | (aot/compile-html [:h1] {})) 47 | 'cljs.core.uix_hoisted__576654167)) 48 | (is (= (binding [env/*compiler* (atom {})] 49 | (aot/compile-html '[:> x {} 1 2] {})) 50 | '(uix.compiler.aot/>el x nil [1 2]))) 51 | (is (= (binding [env/*compiler* (atom {})] 52 | (aot/compile-html '[:> x {:x 1 :ref 2} 1 2] {})) 53 | '(uix.compiler.aot/>el x (js* "{'x':~{},'ref':~{}}" 1 (uix.compiler.alpha/unwrap-ref 2)) [1 2])))) 54 | -------------------------------------------------------------------------------- /core/resources/externs.js: -------------------------------------------------------------------------------- 1 | var React = {} 2 | React.Children = {} 3 | React.createRef = function() {} 4 | React.Component = function() {} 5 | React.PureComponent = function() {} 6 | React.createContext = function() {} 7 | React.forwardRef = function() {} 8 | React.lazy = function() {} 9 | React.memo = function() {} 10 | React.useCallback = function() {} 11 | React.useContext = function() {} 12 | React.useEffect = function() {} 13 | React.useImperativeHandle = function() {} 14 | React.useDebugValue = function() {} 15 | React.useLayoutEffect = function() {} 16 | React.useMemo = function() {} 17 | React.useReducer = function() {} 18 | React.useRef = function() {} 19 | React.useState = function() {} 20 | React.Fragment = function() {} 21 | React.StrictMode = function() {} 22 | React.Suspense = function() {} 23 | React.createElement = function() {} 24 | React.cloneElement = function() {} 25 | React.createFactory = function() {} 26 | React.isValidElement = function() {} 27 | React.version = "" 28 | React.unstable_ConcurrentMode = function() {} 29 | React.unstable_Profiler = function() {} 30 | React.unstable_scheduleCallback = function() {} 31 | React.unstable_cancelCallback = function() {} 32 | React.unstable_now = function() {} 33 | React.unstable_shouldYield = function() {} 34 | React.unstable_runWithPriority = function() {} 35 | React.unstable_getCurrentPriorityLevel = function() {} 36 | React.unstable_ImmediatePriority = function() {} 37 | React.unstable_UserBlockingPriority = function() {} 38 | React.unstable_NormalPriority = function() {} 39 | React.unstable_LowPriority = function() {} 40 | React.unstable_IdlePriority = function() {} 41 | React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {} 42 | 43 | var ReactDOM = {} 44 | ReactDOM.createPortal = function() {} 45 | ReactDOM.findDOMNode = function() {} 46 | ReactDOM.hydrate = function() {} 47 | ReactDOM.render = function() {} 48 | ReactDOM.unstable_renderSubtreeIntoContainer = function() {} 49 | ReactDOM.unmountComponentAtNode = function() {} 50 | ReactDOM.unstable_createPortal = function() {} 51 | ReactDOM.unstable_batchedUpdates = function() {} 52 | ReactDOM.unstable_interactiveUpdates = function() {} 53 | ReactDOM.flushSync = function() {} 54 | ReactDOM.unstable_createRoot = function() {} 55 | ReactDOM.unstable_flushControlled = function() {} 56 | ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {} 57 | 58 | var emotion = {} 59 | emotion.css = function() {} 60 | -------------------------------------------------------------------------------- /core/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"} 2 | org.clojure/clojurescript {:mvn/version "1.10.764"} 3 | cljs-bean/cljs-bean {:mvn/version "1.5.0"} 4 | cljsjs/react {:mvn/version "17.0.2-0" 5 | :exclusions [cljsjs/react-dom-server]}} 6 | :paths ["src"] 7 | :aliases {:dev {:extra-paths ["dev" "resources"] 8 | :extra-deps {uix.dom/uix.dom {:local/root "../dom"} 9 | com.bhauman/figwheel-main {:mvn/version "0.2.1-SNAPSHOT"} 10 | cljsjs/emotion {:mvn/version "10.0.6-0"} 11 | cljsjs/react-dom-server {:mvn/version "17.0.2-0"} 12 | codox/codox {:mvn/version "0.10.6"} 13 | codox-theme-rdash/codox-theme-rdash {:mvn/version "0.1.2"} 14 | aleph/aleph {:mvn/version "0.4.6"} 15 | cljfmt/cljfmt {:mvn/version "0.6.4"} 16 | rhizome/rhizome {:mvn/version "0.2.9"}}} 17 | :rec-front {:extra-deps {com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"}} 18 | :main-opts ["-m" "figwheel.main" "--build" "dev" "--repl" "--serve"]} 19 | :rec-ssr {:main-opts ["-m" "uix.recipes.server-rendering"]} 20 | :benchmark {:extra-paths ["benchmark"] 21 | :extra-deps {reagent/reagent {:mvn/version "0.9.0-SNAPSHOT"} 22 | criterium/criterium {:mvn/version "0.4.5"} 23 | enlive/enlive {:mvn/version "1.1.6"} 24 | hiccup/hiccup {:mvn/version "1.0.5"} 25 | rum/rum {:mvn/version "0.11.2"}}} 26 | :bench-front {:main-opts ["-m" "figwheel.main" "-O" "advanced" "--build" "benchmark" "--serve"]} 27 | :bench-ssr {:main-opts ["-m" "uix.benchmark"]} 28 | :test {:extra-paths ["test"] 29 | :extra-deps {uix.dom/uix.dom {:local/root "../dom"} 30 | cljsjs/react-dom-server {:mvn/version "17.0.2-0"} 31 | clj-diffmatchpatch/clj-diffmatchpatch {:mvn/version "0.0.9.3"}}} 32 | :ci {:extra-paths ["dev"] 33 | :extra-deps {cljfmt/cljfmt {:mvn/version "0.6.4"}}} 34 | :example {:main-opts ["-m" "cljs.main" "-co" "example.cljs.edn" "-c" "uix.example"]} 35 | :release {:extra-deps {appliedscience/deps-library {:mvn/version "0.3.4"}} 36 | :main-opts ["-m" "deps-library.release"]}}} 37 | -------------------------------------------------------------------------------- /dom/src/uix/dom/alpha.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.dom.alpha 2 | "Public API" 3 | (:require #?(:cljs [react-dom :as rdom]) 4 | [uix.compiler.alpha :as compiler])) 5 | 6 | ;; react-dom top-level API 7 | 8 | (defn render 9 | "Renders element into DOM node. The first argument is Hiccup or React element." 10 | [element node] 11 | #?(:cljs 12 | (-> (compiler/as-element element) 13 | (rdom/render node)) 14 | :clj nil)) 15 | 16 | (defn hydrate 17 | "Hydrates server rendered document at `node` with `element`." 18 | [element node] 19 | #?(:cljs (rdom/hydrate (compiler/as-element element) node) 20 | :clj nil)) 21 | 22 | (defn flush-sync! [cb] 23 | #?(:cljs (rdom/flushSync cb) 24 | :clj nil)) 25 | 26 | #?(:clj 27 | (defmacro flush-sync [& body] 28 | `(flush-sync! (fn [] ~@body)))) 29 | 30 | (defn unmount-at-node 31 | "Unmounts React component rendered into DOM node" 32 | [node] 33 | #?(:cljs (rdom/unmountComponentAtNode node) 34 | :clj nil)) 35 | 36 | (defn find-dom-node 37 | "Returns top-level DOM node associated with component" 38 | [component] 39 | #?(:cljs (rdom/findDOMNode component) 40 | :clj nil)) 41 | 42 | (defn create-portal 43 | "Renders Hiccup element into DOM node" 44 | [child node] 45 | #?(:cljs (rdom/createPortal (compiler/as-element child) node) 46 | :clj (prn (str "Portal elements are not supported on JVM, skipping: " [:-> child node])))) 47 | 48 | ;; react-dom/server top-level API 49 | 50 | (defn render-to-string [element] 51 | "Renders to HTML string to be used with React" 52 | #?(:clj (compiler/render-to-string element) 53 | :cljs (.renderToString js/ReactDOMServer (compiler/as-element element)))) 54 | 55 | (defn render-to-static-markup [element] 56 | "Renders to HTML string" 57 | #?(:clj (compiler/render-to-static-markup element) 58 | :cljs (.renderToStaticMarkup js/ReactDOMServer (compiler/as-element element)))) 59 | 60 | #?(:clj 61 | (def render-to-stream 62 | "Like render-to-string, but pushes HTML in chunks as they are being rendered 63 | 64 | (render-to-stream [element] {:on-chunk f})" 65 | compiler/render-to-stream)) 66 | 67 | #?(:cljs 68 | (defn render-to-stream [element] 69 | (.renderToNodeStream js/ReactDOMServer (compiler/as-element element)))) 70 | 71 | #?(:clj 72 | (def render-to-static-stream 73 | "Like render-to-static-markup, but pushes HTML in chunks as they are being rendered 74 | 75 | (render-to-static-stream [element] {:on-chunk f})" 76 | compiler/render-to-static-stream)) 77 | 78 | #?(:cljs 79 | (defn render-to-static-stream [element] 80 | (.renderToStaticNodeStream js/ReactDOMServer (compiler/as-element element)))) 81 | -------------------------------------------------------------------------------- /core/dev/uix/example.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.example 2 | (:require [cljs.js :as cljs] 3 | [uix.core.alpha :as uix.core] 4 | [uix.dom.alpha :as uix.dom])) 5 | 6 | (def st (cljs/empty-state)) 7 | 8 | (defn eval-handler [{:keys [error value]}] 9 | (if error 10 | (throw (str error)) 11 | (println value))) 12 | 13 | (defn browser-load [{:keys [name macros]} cb] 14 | (let [url (str "example-out/" (cljs/ns->relpath name) ".js")] 15 | (-> (js/fetch url) 16 | (.then #(.text %)) 17 | (.then #(cb {:lang :js :source %}))))) 18 | 19 | (defn eval-string [s] 20 | (cljs/eval-str st s nil 21 | {:eval cljs/js-eval 22 | :load browser-load} 23 | eval-handler)) 24 | 25 | ;; ============================================= 26 | 27 | (defn create-editor [node value] 28 | (js/CodeMirror node 29 | #js {:lineNumbers true 30 | :autoCloseBrackets true 31 | :matchBrackets true 32 | :mode "text/x-clojure" 33 | :value value})) 34 | 35 | (defn editor [{:keys [init-value on-change on-eval]}] 36 | (let [editor-ref (uix.core/ref) 37 | _ (uix.core/effect! 38 | (fn [] 39 | (let [editor (create-editor @editor-ref init-value)] 40 | (.on editor "change" #(on-change (.getValue %))))) 41 | [])] 42 | [:div {:style {:flex 1}} 43 | [:div#editor {:ref editor-ref}] 44 | [:button {:on-click on-eval 45 | :style {:margin-top 16 46 | :padding "8px 12px" 47 | :border-radius "3px" 48 | :background-color :blue 49 | :text-transform :uppercase 50 | :font-weight 600 51 | :color :white 52 | :border :none}} 53 | "Evaluate"]])) 54 | 55 | (defn view [] 56 | [:div {:style {:flex 1 57 | :margin-left 16}} 58 | [:div#viewRoot]]) 59 | 60 | (defn root [initial-code] 61 | (let [code (uix.core/state initial-code) 62 | handle-change #(reset! code %)] 63 | (uix.core/effect! 64 | (fn [] 65 | (eval-string initial-code)) 66 | []) 67 | [:div {:style {:height "90%" 68 | :display :flex 69 | :flex-direction :column 70 | :padding 16}} 71 | [:header 72 | [:img {:src "logo.png" :width 125}]] 73 | [:div {:style {:display :flex 74 | :flex 1}} 75 | [editor {:init-value initial-code 76 | :on-change handle-change 77 | :on-eval #(eval-string @code)}] 78 | [view]]])) 79 | 80 | (-> (js/fetch "init.cljs") 81 | (.then #(.text %)) 82 | (.then #(uix.dom/render [root %] (.-root js/window)))) 83 | -------------------------------------------------------------------------------- /core/resources/public/init.cljs: -------------------------------------------------------------------------------- 1 | (require '[uix.core.alpha :as uix.core]) 2 | (require '[uix.dom.alpha :as uix.dom]) 3 | (require '[clojure.string :as str]) 4 | 5 | (defn fetch-repos [uname] 6 | (-> (js/fetch (str "https://api.github.com/users/" uname "/repos")) 7 | (.then #(.json %)) 8 | (.then #(js->clj % :keywordize-keys true)))) 9 | 10 | (defn input [{:keys [value on-change placeholder]}] 11 | [:input {:value value 12 | :placeholder placeholder 13 | :on-change #(-> % .-target .-value on-change) 14 | :style {:display :flex 15 | :font-size "16px" 16 | :padding "4px 8px" 17 | :border-radius 3 18 | :border "1px solid blue"}}]) 19 | 20 | (defn button 21 | ([child] 22 | (button nil child)) 23 | ([{:keys [on-click disabled?]} child] 24 | [:button {:on-click on-click 25 | :disabled disabled? 26 | :style {:padding "8px 12px" 27 | :border-radius 3 28 | :background-color :blue 29 | :text-transform :uppercase 30 | :font-weight 600 31 | :color :white 32 | :border :none}} 33 | child])) 34 | 35 | (defn list-view [{:keys [data render-item key-fn]}] 36 | [:ul 37 | (for [item data] 38 | ^{:key (key-fn item)} 39 | [render-item item])]) 40 | 41 | (defn repo-item [{:keys [name html_url]}] 42 | [:li 43 | [:a {:href html_url} name]]) 44 | 45 | (defn example [] 46 | (let [uname (uix.core/state "") 47 | repos (uix.core/state []) 48 | error (uix.core/state nil) 49 | loading? (uix.core/state false) 50 | handle-submit (fn [e] 51 | (.preventDefault e) 52 | (reset! loading? true) 53 | (-> (fetch-repos @uname) 54 | (.then #(do (reset! repos %) 55 | (reset! loading? false) 56 | (reset! error nil))) 57 | (.catch #(do (reset! loading? false) 58 | (reset! error (.-message %))))))] 59 | [:<> 60 | [:form {:on-submit handle-submit} 61 | [input {:value @uname 62 | :placeholder "GitHub username" 63 | :on-change #(reset! uname %)}] 64 | [:div {:style {:margin-top 16}} 65 | [button {:disabled? (str/blank? @uname)} 66 | "Fetch repos"]]] 67 | (when @loading? 68 | [:div "Loading..."]) 69 | (when-let [msg @error] 70 | [:div {:style {:padding "4px 12px" 71 | :background-color "rgba(255, 0, 0, 0.1)" 72 | :color :red 73 | :border-radius 3}} 74 | msg]) 75 | [list-view {:data @repos 76 | :key-fn :name 77 | :render-item repo-item}]])) 78 | 79 | 80 | (uix.dom/render [example] (.getElementById js/document "viewRoot")) 81 | -------------------------------------------------------------------------------- /core/src/xframe/core/adapton.cljs: -------------------------------------------------------------------------------- 1 | (ns xframe.core.adapton 2 | (:require-macros [xframe.core.adapton :refer [adapt]])) 3 | 4 | (def ^:private curr-adapting (volatile! false)) 5 | 6 | (defprotocol IAdapton 7 | (+edge! [this a-sub]) 8 | (-edge! [this a-sub]) 9 | (compute [this]) 10 | (dirty! [this]) 11 | (set-thunk! [this new-thunk]) 12 | (set-result! [this new-result]) 13 | (get-sup [this]) 14 | (set-sup! [this new-sup]) 15 | (get-result [this])) 16 | 17 | (deftype Adapton [^:mutable thunk 18 | ^:mutable result 19 | ^:mutable sub 20 | ^:mutable sup 21 | ^:mutable clean? 22 | ameta] 23 | IAdapton 24 | (get-sup [this] 25 | sup) 26 | (set-sup! [this new-sup] 27 | (set! sup new-sup)) 28 | (get-result [this] 29 | result) 30 | (+edge! [this a-sub] 31 | (.add sub a-sub) 32 | (.add (.-sup a-sub) this)) 33 | (-edge! [this a-sub] 34 | (.delete sub a-sub) 35 | (.delete (get-sup ^not-native a-sub) this)) 36 | (compute [^not-native this] 37 | (if ^boolean clean? 38 | result 39 | (do 40 | (-> (.from js/Array sub) (.forEach #(-edge! this %))) 41 | (set! clean? true) 42 | (try 43 | (set! result (thunk)) 44 | (catch :default e 45 | (set! result e) 46 | (.error js/console (str "Subscription " (into [(:name ameta)] (:args ameta)) " failed to compute")))) 47 | (recur)))) 48 | (dirty! [this] 49 | (when ^boolean clean? 50 | (set! clean? false) 51 | (-> (.from js/Array sup) (.forEach #(dirty! ^not-native %))))) 52 | (set-thunk! [this new-thunk] 53 | (set! thunk new-thunk)) 54 | (set-result! [this new-result] 55 | (set! result new-result)) 56 | 57 | IDeref 58 | (-deref [this] 59 | (let [prev-adapting (volatile! @curr-adapting) 60 | _ (vreset! curr-adapting this) 61 | result (compute ^not-native this) 62 | _ (vreset! curr-adapting @prev-adapting)] 63 | (when ^boolean @curr-adapting 64 | (+edge! ^not-native @curr-adapting this)) 65 | result)) 66 | 67 | IReset 68 | (-reset! [this v] 69 | (set-result! this v) 70 | (dirty! this) 71 | result) 72 | 73 | ISwap 74 | (-swap! [this f] 75 | (-reset! this (f (-deref this)))) 76 | (-swap! [this f x] 77 | (-reset! this (f (-deref this) x))) 78 | (-swap! [this f x y] 79 | (-reset! this (f (-deref this) x y))) 80 | (-swap! [this f x y args] 81 | (-reset! this (apply f (-deref this) x y args))) 82 | 83 | IMeta 84 | (-meta [this] 85 | ameta)) 86 | 87 | (defn adapton? [v] 88 | (instance? Adapton v)) 89 | 90 | (defn make-athunk [thunk & [meta]] 91 | (Adapton. thunk nil (js/Set.) (js/Set.) false meta)) 92 | 93 | (defn aref [v] 94 | (let [a (Adapton. nil v (js/Set.) (js/Set.) true nil)] 95 | (set-thunk! a #(get-result a)) 96 | a)) 97 | 98 | (defn avar-get [v] 99 | (-deref (-deref v))) 100 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/server_rendering.clj: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.server-rendering 2 | "This recipe shows how to use UIx's server-side rendering capability in JVM Clojure. 3 | 4 | `uix.core.alpha/render-to-string` takes an element and synchronously returns 5 | HTML string that can be sent back to the client. 6 | 7 | `uix.core.alpha/render-to-stream` does the same but provides a way 8 | to consume HTML string in chunks as they are produced, effectively 9 | pushing at you a stream of HTML. This can be used to stream markup 10 | back to the client when serialization takes significant amount of time." 11 | (:require [aleph.http :as http] 12 | [manifold.stream :as s] 13 | [uix.dom.alpha :as uix.dom])) 14 | 15 | (def text 16 | "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.") 17 | 18 | (defn db-fetch [] 19 | (Thread/sleep 10) 20 | text) 21 | 22 | (defn app [n] 23 | (when (pos? n) 24 | [:div n (db-fetch) 25 | (map (constantly [app (dec n)]) 26 | (range n))])) 27 | 28 | (def head 29 | "
1
" (t/as-string [f 1]))))) 34 | 35 | #_(deftest test-require-lazy 36 | (require-lazy '[uix.core.alpha :refer [strict-mode]]) 37 | (is (t/react-element-of-type? strict-mode "react.lazy"))) 38 | 39 | (deftest test-html 40 | (is (t/react-element-of-type? (html [:h1 1]) "react.element"))) 41 | 42 | (deftest test-defui 43 | (defui h1 [child] 44 | [:h1 ^:inline child]) 45 | (is (= (t/as-string [h1 1]) "1
"))) 46 | 47 | (deftest test-as-element 48 | (is (-> (uix.core/as-element [:h1 1]) 49 | (t/react-element-of-type? "react.element")))) 50 | 51 | (deftest test-as-react 52 | (let [ctor (fn [props] 53 | (is (= @#'bean/Bean (type props))) 54 | (:x props)) 55 | ftype (-> (uix.core/as-react ctor) 56 | ^js (r/createElement) 57 | .-type)] 58 | (is (= (ftype #js {:x 1}) 1)))) 59 | 60 | (deftest test-create-error-boundary-1 61 | (let [error->state-called? (atom false) 62 | handle-catch-called? (atom false) 63 | err-b (uix.core/create-error-boundary 64 | {:display-name "err-b-1" 65 | :error->state #(reset! error->state-called? true) 66 | :handle-catch #(reset! handle-catch-called? true)} 67 | (fn [err [done x]] 68 | (is (= nil @err)) 69 | (is (= 1 x)) 70 | (is (= false @error->state-called?)) 71 | (is (= false @handle-catch-called?)) 72 | (done) 73 | x))] 74 | (async done 75 | (t/render [err-b done 1])))) 76 | 77 | (deftest test-create-error-boundary-2 78 | (let [handle-catch (atom nil) 79 | child (fn [] (throw (js/Error. "Hello!"))) 80 | err-b (uix.core/create-error-boundary 81 | {:display-name "err-b-2" 82 | :error->state ex-message 83 | :handle-catch (fn [err info] (reset! handle-catch err))} 84 | (fn [cause [done x child]] 85 | (is (= 1 x)) 86 | (cond 87 | (nil? @cause) child 88 | 89 | (= :recover @cause) 90 | (do 91 | (is (some? @handle-catch)) 92 | (done) 93 | x) 94 | 95 | :else (do (is (= "Hello!" @cause)) 96 | (js/setTimeout #(reset! cause :recover) 20) 97 | x))))] 98 | (async done 99 | (t/render [err-b done 1 [child]])))) 100 | 101 | (deftest test-context 102 | (defcontext *ctx* 0) 103 | (let [child-component (fn [done] 104 | (let [v (uix.core/context *ctx*)] 105 | (is (== v 1)) 106 | (done) 107 | v)) 108 | component (fn [done] 109 | (uix.core/context-provider [*ctx* 1] 110 | [child-component done]))] 111 | (async done 112 | (t/render [component done])))) 113 | 114 | (deftest test-no-memoize 115 | (let [f (fn []) 116 | _ (uix.core/no-memoize! f) 117 | el (uix.core/as-element [f])] 118 | (is (= true (.-uix-no-memo f))) 119 | (when ^boolean goog.DEBUG 120 | (is (not (str/starts-with? (.. ^js el -type -displayName) "memo(")))) 121 | (is (not (identical? (.for js/Symbol "react.memo") (aget (.-type el) "$$typeof")))))) 122 | 123 | (defn -main [] 124 | (run-tests)) 125 | -------------------------------------------------------------------------------- /core/test/uix/adapton_test.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.adapton-test 2 | (:require [clojure.test :refer [deftest is run-tests]] 3 | [xframe.core.adapton :as ad :refer [adapt defavar avar avar-set! defamemo]]) 4 | #?(:clj (:import [java.util Date]))) 5 | 6 | (defn get-time [] 7 | #?(:clj (.getTime (Date.)) 8 | :cljs (.getTime (js/Date.)))) 9 | 10 | (deftest test-reset! 11 | (let [r1 (ad/aref :foo)] 12 | (is (= :bar (reset! r1 :bar))) 13 | (is (= :baz (reset! r1 :baz))))) 14 | 15 | (deftest test-swap! 16 | (let [r1 (ad/aref 0)] 17 | (is (= 1 (swap! r1 inc))) 18 | (is (= 2 (swap! r1 inc))))) 19 | 20 | (deftest test-aref 21 | (let [r1 (ad/aref 8) 22 | r2 (ad/aref 10) 23 | a (ad/make-athunk nil)] 24 | (ad/set-thunk! a (fn [] 25 | (ad/+edge! a r1) 26 | (ad/+edge! a r2) 27 | (- (ad/compute r1) 28 | (ad/compute r2)))) 29 | (is (== -2 (ad/compute a))) 30 | (reset! r1 2) 31 | (is (== -8 (ad/compute a))))) 32 | 33 | (deftest test-adapton? 34 | (is (= true (ad/adapton? (ad/aref 5)))) 35 | (is (= false (ad/adapton? 5)))) 36 | 37 | (deftest test-aforce 38 | (let [r (ad/aref 5) 39 | a (ad/make-athunk #(+ @r 3))] 40 | (is (= 8 @a)) 41 | (reset! r 2) 42 | (is (= 5 @a)))) 43 | 44 | (deftest test-adapt 45 | (let [r1 (ad/aref 2) 46 | r2 (ad/aref (+ @r1 4)) 47 | a (adapt (+ @r1 @r2))] 48 | (is (= 8 @a)) 49 | (reset! r1 10) 50 | (is (= 16 @a)))) 51 | 52 | (deftest test-defavar 53 | (defavar v1 2) 54 | (defavar v2 (+ (ad/avar-get v1) 4)) 55 | (defavar b (+ (ad/avar-get v1) (ad/avar-get v2))) 56 | (is (= 8 (ad/avar-get b))) 57 | (avar-set! v1 10) 58 | (is (= 24 (ad/avar-get b)))) 59 | 60 | (deftest test-tree 61 | (defavar lucky 7) 62 | (defavar t1 [1 2]) 63 | (defavar t2 [3 4]) 64 | (defavar some-tree [(ad/avar-get t1) (ad/avar-get t2)]) 65 | 66 | (defamemo max-tree [t] 67 | (cond 68 | (ad/adapton? t) (recur @t) 69 | (coll? t) (max (max-tree (first t)) 70 | (max-tree (second t))) 71 | :else t)) 72 | 73 | (defamemo max-tree-path [t] 74 | (cond 75 | (ad/adapton? t) (recur @t) 76 | (coll? t) (if (> (max-tree (first t)) 77 | (max-tree (second t))) 78 | (cons 'left (max-tree-path (first t))) 79 | (cons 'right (max-tree-path (second t)))) 80 | :else [])) 81 | 82 | (is (= [[1 2] [3 4]] (ad/avar-get some-tree))) 83 | (is (= 4 (max-tree some-tree))) 84 | (is (= '[right right] (max-tree-path some-tree))) 85 | 86 | (avar-set! t2 5) 87 | (is (= [[1 2] 5] (ad/avar-get some-tree))) 88 | (is (= 5 (max-tree some-tree))) 89 | (is (= '[right] (max-tree-path some-tree))) 90 | (is (= 5 (max-tree (second (ad/avar-get some-tree))))) 91 | (is (= [] (max-tree-path (second (ad/avar-get some-tree))))) 92 | 93 | (avar-set! t2 (vector 20 (* 3 (ad/avar-get lucky)))) 94 | (is (= [[1 2] [20 21]] (ad/avar-get some-tree))) 95 | (is (= 21 (max-tree some-tree))) 96 | (is (= '[right right] (max-tree-path some-tree))) 97 | 98 | (avar-set! lucky 3) 99 | (is (= [[1 2] [20 9]] (ad/avar-get some-tree))) 100 | (is (= 20 (max-tree some-tree))) 101 | (is (= '[right left] (max-tree-path some-tree)))) 102 | 103 | (deftest test-html 104 | (def data 105 | {"4" ["5" "9"], 106 | "0" ["8"], 107 | "1" ["2" "9"], 108 | "2" ["4" "8"], 109 | "7" ["7"], 110 | "9" ["7" "1" "2" "8"], 111 | "8" ["0" "7"], 112 | "5" ["0"]}) 113 | 114 | (def avar-data 115 | (reduce-kv 116 | (fn [ret k v] 117 | (assoc ret k (avar v))) 118 | {} 119 | data)) 120 | 121 | (defn hiccup [data ks max-depth] 122 | (when-not (zero? max-depth) 123 | (into [:ul] (for [k ks] 124 | (if-let [d (hiccup data (get data k) (dec max-depth))] 125 | [:li k d] 126 | [:li k]))))) 127 | 128 | (declare hiccup-memo) 129 | 130 | (defn hiccup-memo* [data ks max-depth] 131 | (when-not (zero? max-depth) 132 | (into [:ul] 133 | (for [k ks] 134 | (if-let [d (hiccup-memo data (get data k) (dec max-depth))] 135 | [:li k d] 136 | [:li k]))))) 137 | 138 | (def hiccup-memo (memoize hiccup-memo*)) 139 | 140 | (defamemo hiccup-amemo [data ks max-depth] 141 | (cond 142 | (ad/adapton? ks) (hiccup-amemo data @ks max-depth) 143 | (zero? max-depth) nil 144 | :else (into [:ul] 145 | (for [k ks] 146 | (if-let [d (hiccup-amemo data (get data k) (dec max-depth))] 147 | [:li k d] 148 | [:li k]))))) 149 | 150 | (is (= (hiccup data ["1"] 20) 151 | (hiccup-memo data ["1"] 20) 152 | (hiccup-amemo avar-data ["1"] 20)))) 153 | -------------------------------------------------------------------------------- /core/benchmark/uix/hiccup.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.hiccup 2 | (:require [uix.core.alpha :refer [html]])) 3 | 4 | (defn input-field [{:keys [field-type type placeholder size] 5 | :or {field-type :input}}] 6 | [field-type 7 | {:class ["form-control" (get {:large "form-control-lg"} size)] 8 | :type type 9 | :placeholder placeholder 10 | :style {:border "1px solid blue" 11 | :border-radius 3 12 | :padding "4px 8px"}}]) 13 | 14 | (defn button [{:keys [size kind class]} child] 15 | [:button.btn 16 | {:class [(get {:large "btn-lg"} size) 17 | (get {:primary "btn-primary"} kind) 18 | class] 19 | :style {:padding "8px 24px" 20 | :color :white 21 | :background :blue 22 | :font-size "11px" 23 | :text-transform :uppercase 24 | :text-align :center}} 25 | child]) 26 | 27 | (defn fieldset [& children] 28 | (into [:fieldset.form-group 29 | {:style {:padding 8 30 | :border :none}}] 31 | children)) 32 | 33 | (defn form [& children] 34 | (into [:form] children)) 35 | 36 | (defn row [& children] 37 | (into [:div.row] children)) 38 | 39 | (defn col [{:keys [md xs offset-md]} & children] 40 | (into 41 | [:div {:class [(str "col-md-" md) 42 | (str "col-xs-" xs) 43 | (str "offset-md-" offset-md)]}] 44 | children)) 45 | 46 | (defn editor [] 47 | [:div.editor-page 48 | [:div.container.page 49 | [row 50 | [col {:md 10 51 | :xs 12 52 | :offset-md 1} 53 | [form 54 | [:fieldset 55 | [fieldset 56 | [input-field {:type "text" 57 | :placeholder "Article Title" 58 | :size :large}]] 59 | [fieldset 60 | [input-field {:type "text" 61 | :placeholder "What's this article about?"}]] 62 | [fieldset 63 | [input-field {:rows "8" 64 | :field-type :textarea 65 | :placeholder "Write your article (in markdown)"}]] 66 | [fieldset 67 | [input-field {:type "text" 68 | :placeholder "Enter tags"}] 69 | [:div.tag-list]] 70 | [button 71 | {:size :large 72 | :kind :primary 73 | :class "pull-xs-right"} 74 | "Update Article"]]]]]]]) 75 | 76 | ;; ==== Pre-compiled components ==== 77 | 78 | (defn input-field-compiled 79 | [{:keys [field-type type placeholder size] 80 | :or {field-type :input}}] 81 | (if (= field-type :textarea) 82 | (html 83 | [:textarea 84 | {:class ["form-control" (get {:large "form-control-lg"} size)] 85 | :type type 86 | :placeholder placeholder 87 | :style {:border "1px solid blue" 88 | :border-radius 3 89 | :padding "4px 8px"}}]) 90 | (html 91 | [:input 92 | {:class ["form-control" (get {:large "form-control-lg"} size)] 93 | :type type 94 | :placeholder placeholder 95 | :style {:border "1px solid blue" 96 | :border-radius 3 97 | :padding "4px 8px"}}]))) 98 | 99 | (defn button-compiled [{:keys [size kind class]} child] 100 | (html 101 | [:button.btn 102 | {:class [(get {:large "btn-lg"} size) 103 | (get {:primary "btn-primary"} kind) 104 | class] 105 | :style {:padding "8px 24px" 106 | :color :white 107 | :background :blue 108 | :font-size "11px" 109 | :text-transform :uppercase 110 | :text-align :center}} 111 | ^:inline child])) 112 | 113 | (defn fieldset-compiled [& children] 114 | (html 115 | [:fieldset.form-group 116 | {:style {:padding 8 117 | :border :none}} 118 | ^:inline children])) 119 | 120 | (defn form-compiled [& children] 121 | (html 122 | [:form ^:inline children])) 123 | 124 | (defn row-compiled [& children] 125 | (html [:div.row ^:inline children])) 126 | 127 | (defn col-compiled [{:keys [md xs offset-md]} & children] 128 | (html 129 | [:div {:class [(str "col-md-" md) 130 | (str "col-xs-" xs) 131 | (str "offset-md-" offset-md)]} 132 | ^:inline children])) 133 | 134 | (defn editor-compiled [] 135 | (html 136 | [:div.editor-page 137 | [:div.container.page 138 | [row-compiled 139 | [col-compiled 140 | {:md 10 141 | :xs 12 142 | :offset-md 1} 143 | [form-compiled 144 | [:fieldset 145 | [fieldset-compiled 146 | [input-field-compiled 147 | {:type "text" 148 | :placeholder "Article Title" 149 | :size :large}]] 150 | [fieldset-compiled 151 | [input-field-compiled 152 | {:type "text" 153 | :placeholder "What's this article about?"}]] 154 | [fieldset-compiled 155 | [input-field-compiled 156 | {:rows "8" 157 | :field-type :textarea 158 | :placeholder "Write your article (in markdown)"}]] 159 | [fieldset-compiled 160 | [input-field-compiled 161 | {:type "text" 162 | :placeholder "Enter tags"}] 163 | [:div.tag-list]] 164 | [button-compiled 165 | {:size :large 166 | :kind :primary 167 | :class "pull-xs-right"} 168 | "Update Article"]]]]]]])) 169 | -------------------------------------------------------------------------------- /core/src/xframe/core/alpha.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-hooks xframe.core.alpha 2 | "EXPERIMENTAL: Global state management based on Adapton 3 | https://github.com/roman01la/adapton 4 | 5 | How it works: 6 | - App db is Adapton ref node 7 | - A subscription is Adapton thunk node 8 | - Subscriptions graph is a graph of Adapton nodes maintained by Adapton itself 9 | 10 | 1. When UI is rendered ` A +---> B +---> [B] 16 | + 17 | +---> C +---> D 18 | 19 | 2. db is updated, calling subscription listener [B] 20 | db +====> A +====> B +====> [B] 21 | + 22 | +---> C +---> D 23 | 24 | Alternatives: 25 | - https://github.com/salsa-rs/salsa" 26 | #?(:cljs (:require-macros [xframe.core.alpha :refer [reg-sub]])) 27 | (:require [uix.core.alpha :as uix] 28 | [xframe.core.adapton :as adapton] 29 | [uix.lib :refer [doseq-loop]])) 30 | 31 | (defonce ^:dynamic db (adapton/aref {})) 32 | 33 | (defonce ^:private subs-registry (atom {})) 34 | 35 | (defn -reg-sub [name f] 36 | (swap! subs-registry assoc name f)) 37 | 38 | #?(:cljs 39 | (def ^:private subs-in-order #js [])) 40 | 41 | (defn notify-listeners! [] 42 | #?(:cljs (.forEach subs-in-order (fn [f] (f))))) 43 | 44 | ;; https://github.com/facebook/react/tree/master/packages/use-subscription#subscribing-to-event-dispatchers 45 | (defn subscribe-ref [get-state] 46 | #?(:clj (get-state) 47 | :cljs (uix/subscribe 48 | (uix/memo 49 | (fn [] 50 | {:get-current-value get-state 51 | :subscribe (fn [schedule-update!] 52 | (.push subs-in-order schedule-update!) 53 | #(let [idx (.indexOf subs-in-order schedule-update!)] 54 | (when-not (neg? idx) 55 | (.splice subs-in-order idx 1))))}) 56 | #js [get-state])))) 57 | 58 | ;; ====== Public API ====== 59 | 60 | (comment 61 | (require 'rhizome.viz) 62 | (rhizome.viz/view-tree (comp seq :children) :children g 63 | :node->descriptor (fn [n] {:label (:name n)}))) 64 | 65 | (defn subs-graph 66 | "Takes db and returns its dependency graph" 67 | [a] 68 | (let [sup #?(:cljs (->> (adapton/get-sup a) .values js/Array.from (mapv subs-graph)) 69 | :clj (->> (adapton/get-sup a) (mapv subs-graph))) 70 | m (meta a)] 71 | {:name (if-let [name (:name m)] 72 | (into [name] (:args m)) 73 | :db) 74 | :children sup 75 | :value (adapton/get-result a)})) 76 | 77 | #?(:clj 78 | (defn memoize-last-by [key-f args-f f] 79 | (let [mem (atom {})] 80 | (fn [& args] 81 | (let [k (key-f args) 82 | args (args-f args) 83 | e (find @mem k)] 84 | (if (and e (= args (first (val e)))) 85 | (second (val e)) 86 | (let [ret (f args)] 87 | (swap! mem assoc k (list args ret)) 88 | ret)))))) 89 | :cljs 90 | (defn memoize-last-by [key-f args-f f] 91 | (let [mem (volatile! {}) 92 | lookup-sentinel #js {}] 93 | (fn [& args] 94 | (let [k (key-f args) 95 | args (args-f args) 96 | v (get @mem k lookup-sentinel)] 97 | (if (or (identical? v lookup-sentinel) 98 | (not= args (aget v 0))) 99 | (let [ret (f args)] 100 | (vswap! mem assoc k #js [args ret]) 101 | ret) 102 | (aget v 1))))))) 103 | 104 | #?(:clj 105 | (defmacro reg-sub [name [_ args & body]] 106 | `(->> (adapton/xf-amemo ~(with-meta args {:name name}) ~@body) 107 | (-reg-sub ~name)))) 108 | 109 | (defn <- 110 | ([s] 111 | (<- s nil)) 112 | ([[name & args] key] 113 | (let [f (get @subs-registry name)] 114 | (assert f (str "Subscription " name " is not found")) 115 | (f key args)))) 116 | 117 | #?(:clj 118 | (defmacro (.. ~'js/__REACT_DEVTOOLS_GLOBAL_HOOK__ -renderers) (.get 1) .getCurrentFiber)) 129 | ~ret 130 | ;; TODO: Put back get-state-sym 131 | ~ret) 132 | ~ret) 133 | ret))))) 134 | 135 | (def event-handlers (volatile! {})) 136 | (def fx-handlers (volatile! {})) 137 | 138 | (defn dispatch [[name :as event]] 139 | (let [handler (get @event-handlers name) 140 | _ (assert handler (str "Event handler " name " is not found")) 141 | effects (handler @db event)] 142 | (when-let [db' (:db effects)] 143 | (let [handler (get @fx-handlers :db)] 144 | (handler nil [nil db']))) 145 | (doseq-loop [[event args] (dissoc effects :db)] 146 | (let [handler (get @fx-handlers event)] 147 | (assert handler (str "Effect handler " event " is not found")) 148 | #?(:clj 149 | (try 150 | (handler @db [event args]) 151 | (catch Exception e 152 | (binding [*out* *err*] 153 | (println (str "Effect handler " event " failed with arguments: ") args) 154 | (println e)))) 155 | :cljs 156 | (try 157 | (handler @db [event args]) 158 | (catch :default e 159 | (.error js/console (str "Effect handler " event " failed") args) 160 | (.error js/console e)))))))) 161 | 162 | (defn reg-event-db [name f] 163 | (vswap! event-handlers assoc name (fn [a b] {:db (f a b)}))) 164 | 165 | (defn reg-event-fx [name f] 166 | (vswap! event-handlers assoc name f)) 167 | 168 | (defn reg-fx [name f] 169 | (vswap! fx-handlers assoc name f)) 170 | 171 | (defn reg-db-sub [] 172 | (reg-sub ::db (fn [] @db))) 173 | 174 | (reg-db-sub) 175 | 176 | (reg-fx :db 177 | (fn [_ [_ db*]] 178 | (reset! db db*) 179 | (notify-listeners!))) 180 | 181 | (reg-fx :dispatch 182 | (fn [_ [_ event]] 183 | (dispatch event))) 184 | 185 | (reg-fx :dispatch-n 186 | (fn [_ [_ events]] 187 | (run! dispatch events))) 188 | 189 | #?(:cljs 190 | (defn ^:before-load reset-db [] 191 | (set! db (adapton/aref @db)) 192 | (reg-db-sub))) 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > UIx v1 is not actively maintained anymore, consider using its successor v2 at [pitch-io/uix](https://github.com/pitch-io/uix) 2 | 3 |
4 |
5 | _Idiomatic ClojureScript interface to modern React.js_
6 |
7 | Discuss at [#uix on Clojurians Slack](http://clojurians.net/). Bug reports, feature requests and PRs are welcome.
8 |
9 | UIx v2 is available at https://github.com/pitch-io/uix
10 |
11 | [Try it in online REPL](https://roman01la.github.io/uix/)
12 |
13 | [Docs and Guides](https://roman01la.gitbook.io/uix/)
14 |
15 | [API Documentation](https://roman01la.github.io/uix/docs/)
16 |
17 | _If you like what I do, consider supporting my work via donation_
18 |
19 | [](https://www.buymeacoffee.com/romanliutikov)
20 |
21 | [](https://circleci.com/gh/roman01la/uix)
22 |
23 | Clojars updates are pushed occasionally, depend via Git deps to get the most recent updates.
24 |
25 | [](https://clojars.org/uix/core)
26 | [](https://clojars.org/uix/dom)
27 | [](https://clojars.org/uix/rn)
28 |
29 | ```clj
30 | {:deps {uix.core {:git/url "https://github.com/roman01la/uix.git"
31 | :deps/root "core"
32 | :sha "{{replace with commit hash}}"}
33 | uix.dom {:git/url "https://github.com/roman01la/uix.git"
34 | :deps/root "dom"
35 | :sha "{{replace with commit hash}}"}
36 | uix.rn {:git/url "https://github.com/roman01la/uix.git"
37 | :deps/root "rn"
38 | :sha "{{replace with commit hash}}"}}}
39 | ```
40 |
41 | ```clj
42 | (require '[uix.core.alpha :as uix])
43 | (require '[uix.dom.alpha :as uix.dom])
44 |
45 | (defn button [{:keys [on-click]} text]
46 | [:button.btn {:on-click on-click}
47 | text])
48 |
49 | (defn app []
50 | (let [state* (uix/state 0)]
51 | [:<>
52 | [button {:on-click #(swap! state* dec)} "-"]
53 | [:span @state*]
54 | [button {:on-click #(swap! state* inc)} "+"]]))
55 |
56 | (uix.dom/render [app] js/root)
57 | ```
58 |
59 | ## Recipes
60 |
61 | - [State hook](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/state_hook.cljc)
62 | - [Global state and effects](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/global_state.cljc)
63 | - [Dynamic styles](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/dynamic_styles.cljc)
64 | - [Lazy loading](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/lazy_loading.cljc)
65 | - [Server-side rendering](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/server_rendering.clj)
66 | - [Interop between UIx and JS components](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/interop.cljc)
67 | - [Popups](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/popup.cljc)
68 | - [Error Boundaries](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/error_boundary.cljc)
69 | - [React Context](https://github.com/roman01la/uix/blob/master/core/dev/uix/recipes/context.cljc)
70 |
71 | - Build front-end `clojure -A:dev -m figwheel.main -O advanced -bo dev:prod`
72 | - Run server `clojure -A:dev -m uix.server`
73 | - Run front-end recipes in dev `clojure -A:dev:rec-front`
74 | - Run SSR streaming recipe `clojure -A:dev:rec-ssr`
75 |
76 | ## Features
77 |
78 | ### Hiccup syntax extension
79 |
80 | - `[:div#id.class]` or `[:#id.class]`
81 | - `[:> js/Component attrs & children]` - interop with JS components
82 | - `[:<> attrs & children]` - `React.Fragment`
83 | - `[:# {:fallback element} & children]` - `React.Suspense`
84 |
85 | ### Hooks
86 |
87 | React Hooks in idiomatic Clojure style
88 |
89 | ```clj
90 | ;; state hook
91 | ;; (mutable ref type, re-renders component when mutated)
92 | (let [state (uix/state 0)]
93 | (swap! state inc)
94 | @state) ; 1
95 |
96 | ;; ref hook
97 | ;; (mutable ref type, doesn't cause re-renders)
98 | (let [ref (uix/ref 0)]
99 | (swap! ref inc)
100 | @ref) ; 1
101 |
102 | ;; effect hook
103 | (uix/effect!
104 | (fn []
105 | (prn "after update")
106 | #(prn "before unmount"))
107 | [deps])
108 |
109 | ;; convenience macro for uix.core/effect!
110 | (uix/with-effect [deps]
111 | (prn "after update")
112 | #(prn "before unmount"))
113 |
114 | ;; more in uix.core.alpha ns
115 | ```
116 |
117 | ### Attributes syntax extension
118 |
119 | Injects provided function into attributes transformation stage. Could be used for various side effects, such as processing styles with CSS-in-JS libraries (see `uix.recipes.dynamic-styles`).
120 |
121 | ```clj
122 | (uix.core.alpha/add-transform-fn
123 | (fn [attrs]
124 | (my-transform-attrs attrs)))
125 | ```
126 |
127 | ### Hiccup pre-compilation (advanced)
128 |
129 | _NOTE: UIx interpreter is already super fast (3x faster than Reagent and only 2x slower than vanilla React).
130 | Use pre-compilation ONLY if you are hitting performance problems._
131 |
132 | Compiles Hiccup into inlined React elements at compile-time and hoists constant elements so they can be shared across components in different namespaces (for reference see [@babel/plugin-transform-react-inline-elements](https://babeljs.io/docs/en/babel-plugin-transform-react-inline-elements) and [@babel/plugin-transform-react-constant-elements](https://babeljs.io/docs/en/babel-plugin-transform-react-constant-elements)). Hoisting is enabled with `:optimize-constants` compiler option, which is automatically enabled for `:optimizations :advanced`.
133 |
134 | ```clj
135 | (uix/html
136 | [:h1 "Title"])
137 |
138 | ;; emits this
139 | {
140 | $$typeof: Symbol.for("react.element"),
141 | key: null,
142 | ref: null,
143 | props: { children: "Title" },
144 | _owner: null
145 | }
146 | ```
147 |
148 | ### Lazy loading components
149 |
150 | Loading React components on-demand as Closure modules. See [code splitting](https://clojurescript.org/guides/code-splitting) guide and how lazy loading is used in React with Suspense: [guide](https://reactjs.org/docs/code-splitting.html).
151 |
152 | ```clj
153 | (uix.core.lazy-loader/require-lazy
154 | '[uix.components :refer [ui-list]])
155 |
156 | [:# {:fallback "Loading..."}
157 | (when show?
158 | [ui-list])]
159 | ```
160 |
161 | ### Server-side rendering
162 |
163 | UIx can be used for SSR or usual templating in both JVM and JavaScript runtimes
164 |
165 | #### Server-side rendering in JVM
166 |
167 | See an example in `uix.recipes.server-rendering`
168 |
169 | ```clj
170 | (uix.dom/render-to-string element) ;; see https://reactjs.org/docs/react-dom-server.html#rendertostring
171 | (uix.dom/render-to-static-markup element) ;; see https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup
172 |
173 | ;; Streaming HTML
174 | (uix.dom/render-to-stream element {:on-chunk f}) ;; see https://reactjs.org/docs/react-dom-server.html#rendertonodestream
175 | (uix.dom/render-to-static-stream element {:on-chunk f}) ;; see https://reactjs.org/docs/react-dom-server.html#rendertostaticnodestream
176 | ```
177 |
178 | #### Server-side rendering in JS
179 |
180 | SSR works in JavaScript environment via React's serializer using same API.
181 |
182 | 1. Add `ReactDOMServer` into your dependencies (as `cljsjs/react-dom-server` or any other way)
183 | 2. Run `(uix.dom/render-to-string element)`
184 |
185 | ## Benchmarks
186 |
187 | - Hiccup interpretation `clojure -A:dev:benchmark:bench-front`
188 | - SSR on JVM `clojure -A:dev:benchmark:bench-ssr`
189 |
190 | ### Hiccup interpretation
191 |
192 | ```
193 | react x 23866 ops/s, elapsed 419ms
194 | uix-interpret x 11848 ops/s, elapsed 844ms
195 | reagent-interpret x 4031 ops/s, elapsed 2481ms
196 | ```
197 |
198 | ### SSR on JVM
199 |
200 | | lib | test 1 | test 2 | test 3 |
201 | | ------------- | -------- | ------ | ------- |
202 | | rum | 107.8 µs | 3.6 ms | 7.7 ms |
203 | | uix | 120.8 µs | 3.8 ms | 8.1 ms |
204 | | uix streaming | 115.7 µs | 3.4 ms | 7.6 ms |
205 | | hiccup | 205.7 µs | 6.5 ms | 16.6 ms |
206 |
207 | ### TodoMVC bundle size
208 |
209 | | lib | size | gzip |
210 | | ------- | ----- | ---- |
211 | | rum | 254KB | 70KB |
212 | | reagent | 269KB | 74KB |
213 | | uix | 234KB | 65KB |
214 |
215 | ## Figwheel
216 |
217 | When developing with Figwheel it is recommended to mark root render function with `^:after-load` meta, so Figwheel can update UI tree once the code was re-evaluated.
218 |
219 | ```clj
220 | (ns ^:figwheel-hooks my.ns)
221 |
222 | (defn ^:after-load render []
223 | (uix.dom/render [app] js/root))
224 | ```
225 |
226 | ## React DevTools
227 |
228 | When inspecting UI tree in React DevTools, filter out `memo` components to get cleaner view of components tree.
229 |
230 |
231 |
232 | ## Testing
233 |
234 | ```
235 | scripts/test
236 | ```
237 |
238 | _Note: to ensure you're using the right Node.js version, you can use [nvm](https://github.com/nvm-sh/nvm) and run `nvm use`
239 | once in the directory. Otherwise the Node.js version you use is in the `.nvmrc` file. See nvm repo for more documentation._
240 |
241 | ## Who’s using UIx
242 |
243 | - [Zeal (REPL meets Clipboard Manager)](https://github.com/den1k/zeal)
244 | - [Floor Planner](http://floor-planner.surge.sh/)
245 | - [Happy Paw mobile web app](https://github.com/roman01la/happy-paw)
246 | - [ProtonNative app](https://github.com/roman01la/proton-native-cljs)
247 | - [Lumber](https://lumber.dev) - [source](https://github.com/lumberdev/lumber-site)
248 | - [GRID](https://theshopgrid.com)
249 | - [ogre.tools - virtual tabletop](https://ogre.tools/)
250 |
--------------------------------------------------------------------------------
/core/src/uix/hooks/alpha.cljc:
--------------------------------------------------------------------------------
1 | (ns uix.hooks.alpha
2 | "Wrappers for React.js Hooks"
3 | (:refer-clojure :exclude [ref])
4 | #?(:cljs (:require-macros [uix.hooks.alpha :refer [maybe-js-deps with-deps-check]]))
5 | #?(:cljs (:require [react :as r]
6 | [goog.object :as gobj])))
7 |
8 | ;; == State hook ==
9 | #?(:cljs
10 | (deftype StateHook [value set-value]
11 | Object
12 | (equiv [this other]
13 | (-equiv this other))
14 |
15 | IHash
16 | (-hash [o] (goog/getUid o))
17 |
18 | IDeref
19 | (-deref [o]
20 | value)
21 |
22 | IReset
23 | (-reset! [o new-value]
24 | (set-value new-value))
25 |
26 | ISwap
27 | (-swap! [o f]
28 | (set-value f))
29 | (-swap! [o f a]
30 | (set-value #(f % a)))
31 | (-swap! [o f a b]
32 | (set-value #(f % a b)))
33 | (-swap! [o f a b xs]
34 | (set-value #(apply f % a b xs)))
35 |
36 | IPrintWithWriter
37 | (-pr-writer [o writer opts]
38 | (-write writer "#object [uix.hooks.alpha.StateHook ")
39 | (pr-writer {:val value} writer opts)
40 | (-write writer "]"))))
41 |
42 | (defn state [value]
43 | #?(:cljs (let [[value set-value] (r/useState value)
44 | sh (r/useMemo #(StateHook. value set-value) #js [])]
45 | (r/useMemo (fn []
46 | (set! (.-value sh) value)
47 | (set! (.-set-value sh) set-value)
48 | sh)
49 | #js [value set-value]))
50 | :clj (atom value)))
51 |
52 | (defn reducer
53 | ([f initial-value]
54 | (reducer f initial-value identity))
55 | ([f initial-value init-f]
56 | #?(:cljs (r/useReducer #(f %1 %2) initial-value init-f)
57 | :clj (let [ref (atom (init-f initial-value))]
58 | [@ref #(swap! ref f %)]))))
59 |
60 | (defprotocol IRef
61 | (unwrap [this]))
62 |
63 | ;; == Ref hook
64 | #?(:cljs
65 | (deftype RefHook [rref]
66 | IRef
67 | (unwrap [this]
68 | rref)
69 |
70 | Object
71 | (equiv [this other]
72 | (-equiv this other))
73 |
74 | IHash
75 | (-hash [o] (goog/getUid o))
76 |
77 | IDeref
78 | (-deref [o]
79 | (gobj/get rref "current"))
80 |
81 | IReset
82 | (-reset! [o new-value]
83 | (gobj/set rref "current" new-value)
84 | new-value)
85 |
86 | ISwap
87 | (-swap! [o f]
88 | (-reset! o (f (-deref o))))
89 | (-swap! [o f a]
90 | (-reset! o (f (-deref o) a)))
91 | (-swap! [o f a b]
92 | (-reset! o (f (-deref o) a b)))
93 | (-swap! [o f a b xs]
94 | (-reset! o (apply f (-deref o) a b xs)))
95 |
96 | IPrintWithWriter
97 | (-pr-writer [o writer opts]
98 | (-write writer "#object [uix.hooks.alpha.RefHook ")
99 | (pr-writer {:val (-deref o)} writer opts)
100 | (-write writer "]"))))
101 |
102 | (defn ref [value]
103 | #?(:cljs (let [vref (r/useRef value)]
104 | (r/useMemo #(RefHook. vref) #js []))
105 | :clj (atom value)))
106 |
107 | #?(:clj
108 | (defmacro maybe-js-deps [deps]
109 | `(if ~deps (into-array ~deps) js/undefined)))
110 |
111 | #?(:clj
112 | (defmacro with-deps-check [[prev-deps] f deps]
113 | `(let [~prev-deps (ref ~deps)]
114 | (when (not= @~prev-deps ~deps)
115 | (reset! ~prev-deps ~deps))
116 | ~f)))
117 |
118 | ;; == Effect hook ==
119 | (defn effect!
120 | ([setup-fn]
121 | #?(:cljs (r/useEffect
122 | #(let [ret (setup-fn)]
123 | (if (fn? ret) ret js/undefined)))))
124 | ([setup-fn deps]
125 | #?(:cljs (with-deps-check [prev-deps*]
126 | (r/useEffect
127 | (fn []
128 | (reset! prev-deps* deps)
129 | (let [ret (setup-fn)]
130 | (if (fn? ret) ret js/undefined)))
131 | (maybe-js-deps @prev-deps*))
132 | deps))))
133 |
134 | #?(:clj
135 | (defmacro with-effect
136 | "Takes optional vector of dependencies and body to be executed in an effect."
137 | [deps & body]
138 | (let [[deps setup-fn] (if (vector? deps)
139 | [deps body]
140 | [nil (cons deps body)])]
141 | `(effect! #(do ~@setup-fn) ~deps))))
142 |
143 | ;; == Layout effect hook ==
144 | (defn layout-effect!
145 | ([setup-fn]
146 | #?(:cljs (r/useLayoutEffect
147 | #(let [ret (setup-fn)]
148 | (if (fn? ret) ret js/undefined)))))
149 | ([setup-fn deps]
150 | #?(:cljs (with-deps-check [prev-deps*]
151 | (r/useLayoutEffect
152 | (fn []
153 | (reset! prev-deps* deps)
154 | (let [ret (setup-fn)]
155 | (if (fn? ret) ret js/undefined)))
156 | (maybe-js-deps @prev-deps*))
157 | deps))))
158 |
159 | #?(:clj
160 | (defmacro with-layout-effect
161 | "Takes optional vector of dependencies and body to be executed in a layout effect."
162 | [deps & body]
163 | (let [[deps setup-fn] (if (vector? deps)
164 | [deps body]
165 | [nil (cons deps body)])]
166 | `(layout-effect! #(do ~@setup-fn) ~deps))))
167 |
168 | ;; == Callback hook ==
169 | (defn callback
170 | ([f]
171 | #?(:cljs (r/useCallback f)
172 | :clj f))
173 | ([f deps]
174 | #?(:cljs (with-deps-check [prev-deps*]
175 | (r/useCallback f (maybe-js-deps @prev-deps*))
176 | deps)
177 | :clj f)))
178 |
179 | ;; == Memo hook ==
180 | (defn memo
181 | ([f]
182 | #?(:cljs (r/useMemo f)
183 | :clj (f)))
184 | ([f deps]
185 | #?(:cljs (with-deps-check [prev-deps*]
186 | (r/useMemo f (maybe-js-deps @prev-deps*))
187 | deps)
188 | :clj (f))))
189 |
190 | ;; == Context hook ==
191 | (defn context [v]
192 | #?(:cljs (r/useContext v)
193 | :clj v))
194 |
195 | ;; == Imperative Handle hook ==
196 | (defn imperative-handle [ref create-handle deps]
197 | #?(:cljs (with-deps-check [prev-deps*]
198 | (r/useImperativeHandle ref create-handle (maybe-js-deps @prev-deps*))
199 | deps)))
200 |
201 | ;; == Debug hook ==
202 | (defn debug
203 | ([v]
204 | (debug v nil))
205 | ([v fmt]
206 | #?(:cljs (r/useDebugValue v fmt))))
207 |
208 | #?(:cljs
209 | (def batched-update
210 | (if (exists? js/ReactDOM)
211 | (.-unstable_batchedUpdates js/ReactDOM)
212 | (fn [f] (f)))))
213 |
214 | ;; == Subscription ==
215 | ;; https://github.com/facebook/react/tree/master/packages/use-subscription
216 | (defn subscribe [{:keys [get-current-value subscribe]}]
217 | #?(:clj (get-current-value)
218 | :cljs (let [get-initial-state (r/useCallback (fn [] #js {:get-current-value get-current-value
219 | :subscribe subscribe
220 | :value (get-current-value)})
221 | #js [get-current-value subscribe])
222 | [state set-state] (r/useState get-initial-state)
223 | ret-value (if (or (not (identical? (gobj/get state "get-current-value") get-current-value))
224 | (not (identical? (gobj/get state "subscribe") subscribe)))
225 | (let [ret-val (get-current-value)]
226 | (set-state #js {:get-current-value get-current-value
227 | :subscribe subscribe
228 | :value ret-val})
229 | ret-val)
230 | (gobj/get state "value"))]
231 | (r/useDebugValue ret-value)
232 | (r/useEffect
233 | (fn []
234 | (let [did-unsubscribe? (volatile! false)
235 | check-for-updates (fn []
236 | (when-not ^boolean @did-unsubscribe?
237 | (let [value (get-current-value)]
238 | (batched-update
239 | (fn []
240 | (set-state
241 | #(if (or (not (identical? (gobj/get % "get-current-value") get-current-value))
242 | (not (identical? (gobj/get % "subscribe") subscribe))
243 | (= (gobj/get % "value") value))
244 | %
245 | (.assign js/Object #js {} % #js {:value value}))))))))
246 | unsubscribe (subscribe check-for-updates)]
247 | (check-for-updates)
248 | (fn []
249 | (vreset! did-unsubscribe? true)
250 | (unsubscribe))))
251 | #js [get-current-value subscribe])
252 | ret-value)))
253 |
254 | ;; == Derived state hook ==
255 | #?(:cljs
256 | (deftype Cursor [ref path]
257 | Object
258 | (equiv [this other]
259 | (-equiv this other))
260 |
261 | IHash
262 | (-hash [o] (goog/getUid o))
263 |
264 | IDeref
265 | (-deref [o]
266 | (get-in @ref path))
267 |
268 | IReset
269 | (-reset! [o new-value]
270 | (swap! ref update-in path (constantly new-value)))
271 |
272 | ISwap
273 | (-swap! [o f]
274 | (-reset! o (f (-deref o))))
275 | (-swap! [o f a]
276 | (-reset! o (f (-deref o) a)))
277 | (-swap! [o f a b]
278 | (-reset! o (f (-deref o) a b)))
279 | (-swap! [o f a b xs]
280 | (-reset! o (apply f (-deref o) a b xs)))
281 |
282 | IPrintWithWriter
283 | (-pr-writer [o writer opts]
284 | (-write writer "#object [uix.hooks.alpha.Cursor ")
285 | (pr-writer {:val (-deref o)} writer opts)
286 | (-write writer "]"))))
287 |
288 | (defn cursor-in [ref path]
289 | #?(:clj (atom (get-in @ref path))
290 | :cljs (memo #(Cursor. ref path) [ref path])))
291 |
--------------------------------------------------------------------------------
/core/src/uix/core/alpha.cljc:
--------------------------------------------------------------------------------
1 | (ns uix.core.alpha
2 | "Public API"
3 | (:refer-clojure :exclude [ref memoize])
4 | #?(:cljs (:require-macros [uix.core.alpha]))
5 | (:require #?(:cljs [react :as r])
6 | [uix.compiler.alpha :as compiler]
7 | [uix.compiler.aot :as uixr]
8 | [uix.lib :refer [doseq-loop]]
9 | [uix.hooks.alpha :as hooks]))
10 |
11 | ;; See https://twitter.com/roman01la/status/1182405182057058314?s=20
12 | ;; for context
13 | #?(:cljs
14 | (when (and ^boolean goog.DEBUG
15 | (exists? js/__REACT_DEVTOOLS_GLOBAL_HOOK__))
16 | (defonce __devtools-hook
17 | (let [value (volatile! nil)
18 | react-type-setter (fn [v]
19 | (vreset! value v))
20 | react-type-getter (fn []
21 | (if-let [uixf (.-uixf @value)]
22 | uixf
23 | @value))
24 | config #js {:get react-type-getter
25 | :set react-type-setter}]
26 | (.defineProperty js/Object js/window "$type" config)))))
27 |
28 | (declare as-element)
29 |
30 | ;; React's top-level API
31 |
32 | (defn strict-mode [child]
33 | #?(:cljs [:> r/StrictMode child]
34 | :clj child))
35 |
36 | (defn profiler [child {:keys [id on-render] :as attrs}]
37 | #?(:cljs [:> r/Profiler attrs child]
38 | :clj child))
39 |
40 | #?(:cljs
41 | (defn create-class
42 | "Creates class based React component"
43 | [{:keys [constructor static prototype]}]
44 | (let [ctor (fn [props]
45 | (this-as this
46 | (.apply r/Component this (js-arguments))
47 | (when constructor
48 | (constructor this props)))
49 | nil)]
50 | (set! (.-prototype ctor) (.create js/Object (.-prototype r/Component)))
51 | (doseq-loop [[k v] static]
52 | (aset ctor (name k) v))
53 | (doseq-loop [[k v] prototype]
54 | (aset (.-prototype ctor) (name k) v))
55 | ctor)))
56 |
57 | (defn create-error-boundary
58 | "Creates React's Error Boundary component
59 |
60 | display-name — the name of the component to be displayed in stack trace
61 | error->state — maps error object to component's state that is used in render-fn
62 | handle-catch — for side-effects, logging etc.
63 | render-fn — takes state value returned from error->state and a vector of arguments passed into error boundary"
64 | [{:keys [display-name error->state handle-catch]
65 | :or {display-name (str (gensym "error-boundary"))}}
66 | render-fn]
67 | #?(:cljs
68 | (let [constructor (fn [^js/React.Component this _]
69 | (set! (.-state this) #js {:argv nil})
70 | (specify! (.-state this)
71 | IDeref
72 | (-deref [o]
73 | (.. this -state -argv))
74 | IReset
75 | (-reset! [o new-value]
76 | (.setState this #js {:argv new-value})
77 | new-value)
78 | ISwap
79 | (-swap!
80 | ([o f]
81 | (-reset! o (f (-deref o))))
82 | ([o f a]
83 | (-reset! o (f (-deref o) a)))
84 | ([o f a b]
85 | (-reset! o (f (-deref o) a b)))
86 | ([o f a b xs]
87 | (-reset! o (apply f (-deref o) a b xs))))))
88 | derive-state (fn [error] #js {:argv (error->state error)})
89 | render (fn []
90 | (this-as this
91 | (let [args (.. this -props -argv)
92 | state (.-state this)]
93 | (-> (render-fn state args)
94 | as-element))))
95 | klass (create-class {:constructor constructor
96 | :static {:displayName display-name
97 | :getDerivedStateFromError derive-state}
98 | :prototype {:componentDidCatch handle-catch
99 | :render render}})]
100 | (fn [& args]
101 | (r/createElement klass #js {:argv args})))
102 |
103 | :clj ^::error-boundary {:display-name display-name
104 | :render-fn render-fn
105 | :handle-catch handle-catch
106 | :error->state error->state}))
107 |
108 | #?(:cljs
109 | (deftype ReactRef [current]
110 | Object
111 | (equiv [this other]
112 | (-equiv this other))
113 |
114 | IHash
115 | (-hash [o] (goog/getUid o))
116 |
117 | IDeref
118 | (-deref [o]
119 | current)
120 |
121 | IPrintWithWriter
122 | (-pr-writer [o writer opts]
123 | (-write writer "#object [uix.core.alpha.ReactRef ")
124 | (pr-writer {:val (-deref o)} writer opts)
125 | (-write writer "]"))))
126 |
127 | (defn create-ref
128 | "Creates React's ref type object."
129 | ([]
130 | (create-ref nil))
131 | ([v]
132 | #?(:cljs (ReactRef. v)
133 | :clj (atom v))))
134 |
135 | (defn memoize
136 | "Takes component `f` and comparator function `should-update?`
137 | that takes previous and next props of the component.
138 | Returns memoized `f`.
139 |
140 | When `should-update?` is not provided uses default comparator
141 | that compares props with clojure.core/=
142 |
143 | UIx components are memoized by default"
144 | ([f]
145 | (memoize f #?(:cljs compiler/*default-compare-args*
146 | :clj nil)))
147 | ([f should-update?]
148 | #?(:cljs (react/memo #(compiler/as-element (apply f (next (.-argv %))))
149 | should-update?)
150 | :clj f)))
151 |
152 | (defn no-memoize!
153 | "Disables memoization of the `f` component"
154 | [f]
155 | #?(:cljs (set! (.-uix-no-memo f) true)))
156 |
157 | (defn state
158 | "Takes initial value and returns React's state hook wrapped in atom-like type."
159 | [value]
160 | (hooks/state value))
161 |
162 | (defn reducer
163 | "Takes state setting fn, initial state value and optional initializer fn.
164 | Returns a tuple of current state value dispatch function."
165 | ([f initial-value]
166 | (hooks/reducer f initial-value))
167 | ([f initial-value init-f]
168 | (hooks/reducer f initial-value init-f)))
169 |
170 | (defn cursor-in
171 | "Takes ref type value and path vector and returns ref type cursor value watching into original ref"
172 | [ref path]
173 | (hooks/cursor-in ref path))
174 |
175 | (defn effect!
176 | "Takes a function to be executed in an effect and optional vector of dependencies.
177 |
178 | See: https://reactjs.org/docs/hooks-reference.html#useeffect"
179 | ([setup-fn]
180 | (hooks/effect! setup-fn))
181 | ([setup-fn deps]
182 | (hooks/effect! setup-fn deps)))
183 |
184 | (defn layout-effect!
185 | "Takes a function to be executed in a layout effect and optional vector of dependencies.
186 |
187 | See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect"
188 | ([setup-fn]
189 | (hooks/layout-effect! setup-fn))
190 | ([setup-fn deps]
191 | (hooks/layout-effect! setup-fn deps)))
192 |
193 | (defn memo
194 | "Takes function f and optional vector of dependencies, and returns memoized f."
195 | ([f]
196 | (hooks/memo f))
197 | ([f deps]
198 | (hooks/memo f deps)))
199 |
200 | (defn ref
201 | "Takes optional initial value and returns React's ref hook wrapped in atom-like type."
202 | ([]
203 | (hooks/ref nil))
204 | ([value]
205 | (hooks/ref value)))
206 |
207 | (defn callback
208 | "Takes function f and optional vector of dependencies, and returns f."
209 | ([f]
210 | (hooks/callback f))
211 | ([f deps]
212 | (hooks/callback f deps)))
213 |
214 | (defn subscribe
215 | "subscribe - fn, takes callback, sets up a listener on external event emitter
216 | which calls the callback and returns a function that unsets the listener.
217 |
218 | get-current-value - fn, returns current state of the external event emitter"
219 | [{:keys [get-current-value subscribe] :as subscription}]
220 | (hooks/subscribe subscription))
221 |
222 | #?(:cljs
223 | (defn create-context [v]
224 | (react/createContext v)))
225 |
226 | #?(:clj
227 | (defmacro defcontext
228 | "cljs: Creates React context with initial value set to `value`.
229 | clj: Create dynamic var bound to `value`."
230 | ([name]
231 | (if (uix.lib/cljs-env? &env)
232 | `(def ~(with-meta name {:dynamic true}) (create-context nil))
233 | `(def ~(with-meta name {:dynamic true}))))
234 | ([name value]
235 | (if (uix.lib/cljs-env? &env)
236 | `(def ~(with-meta name {:dynamic true}) (create-context ~value))
237 | `(def ~(with-meta name {:dynamic true}) ~value)))))
238 |
239 | (defn context
240 | "Takes React context and returns its current value"
241 | [v]
242 | (hooks/context v))
243 |
244 | #?(:clj
245 | (defmacro context-provider
246 | "Takes a binding form where `ctx` is React context and `value` is a supplied value
247 | and any number of and child components.
248 | cljs: Injects provided value into the context for current components sub-tree.
249 | clj: Creates new bindings for `ctx` with supplied `value`, see clojure.core/binding "
250 | [[ctx value] & children]
251 | (if (uix.lib/cljs-env? &env)
252 | (into [:> `(.-Provider ~ctx) {:value value}]
253 | children)
254 | `(binding [~ctx ~value]
255 | ~(into [:uix.core.alpha/bind-context `(fn [f#] (binding [~ctx ~value] (f#)))] children)))))
256 |
257 | #?(:clj
258 | (defmacro with-effect
259 | "Convenience macro for effect hook."
260 | [deps & body]
261 | `(hooks/with-effect ~deps ~@body)))
262 |
263 | #?(:clj
264 | (defmacro with-layout-effect
265 | "Convenience macro for layout effect hook."
266 | [deps & body]
267 | `(hooks/with-layout-effect ~deps ~@body)))
268 |
269 | #?(:clj
270 | (defmacro html
271 | "Compiles Hiccup into React elements at compile-time."
272 | [expr]
273 | (uixr/compile-html expr &env)))
274 |
275 | #?(:clj
276 | (defmacro defui
277 | "Compiles UIx component into React component at compile-time."
278 | [sym args & body]
279 | (if-not (uix.lib/cljs-env? &env)
280 | `(defn ~sym ~args ~@body)
281 | `(defn ~sym ~args
282 | (uixr/compile-defui ~sym ~body)))))
283 |
284 | (defn as-element
285 | "Compiles Hiccup into React elements at run-time."
286 | [x]
287 | #?(:cljs (compiler/as-element x)
288 | :clj x))
289 |
290 | (defn as-react
291 | "Interop with React components. Takes UIx component function and returns same component wrapped into interop layer."
292 | [f]
293 | #?(:cljs (compiler/as-react f)
294 | :clj f))
295 |
296 | (defn add-transform-fn [f]
297 | "Injects attributes transforming function for Hiccup elements pre-transformations"
298 | (compiler/add-transform-fn f))
299 |
--------------------------------------------------------------------------------
/core/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | agent-base@^4.3.0:
6 | version "4.3.0"
7 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
8 | integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
9 | dependencies:
10 | es6-promisify "^5.0.0"
11 |
12 | async-limiter@~1.0.0:
13 | version "1.0.1"
14 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
15 | integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
16 |
17 | balanced-match@^1.0.0:
18 | version "1.0.0"
19 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
20 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
21 |
22 | brace-expansion@^1.1.7:
23 | version "1.1.11"
24 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
25 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
26 | dependencies:
27 | balanced-match "^1.0.0"
28 | concat-map "0.0.1"
29 |
30 | buffer-from@^1.0.0:
31 | version "1.1.1"
32 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
33 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
34 |
35 | concat-map@0.0.1:
36 | version "0.0.1"
37 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
38 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
39 |
40 | concat-stream@1.6.2:
41 | version "1.6.2"
42 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
43 | integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
44 | dependencies:
45 | buffer-from "^1.0.0"
46 | inherits "^2.0.3"
47 | readable-stream "^2.2.2"
48 | typedarray "^0.0.6"
49 |
50 | core-util-is@~1.0.0:
51 | version "1.0.2"
52 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
53 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
54 |
55 | debug@2.6.9:
56 | version "2.6.9"
57 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
58 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
59 | dependencies:
60 | ms "2.0.0"
61 |
62 | debug@^3.1.0:
63 | version "3.2.6"
64 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
65 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
66 | dependencies:
67 | ms "^2.1.1"
68 |
69 | debug@^4.1.0:
70 | version "4.1.1"
71 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
72 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
73 | dependencies:
74 | ms "^2.1.1"
75 |
76 | es6-promise@^4.0.3:
77 | version "4.2.8"
78 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
79 | integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
80 |
81 | es6-promisify@^5.0.0:
82 | version "5.0.0"
83 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
84 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
85 | dependencies:
86 | es6-promise "^4.0.3"
87 |
88 | extract-zip@^1.6.6:
89 | version "1.6.7"
90 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
91 | integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
92 | dependencies:
93 | concat-stream "1.6.2"
94 | debug "2.6.9"
95 | mkdirp "0.5.1"
96 | yauzl "2.4.1"
97 |
98 | fd-slicer@~1.0.1:
99 | version "1.0.1"
100 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
101 | integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
102 | dependencies:
103 | pend "~1.2.0"
104 |
105 | fs.realpath@^1.0.0:
106 | version "1.0.0"
107 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
108 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
109 |
110 | glob@^7.1.3:
111 | version "7.1.4"
112 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
113 | integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
114 | dependencies:
115 | fs.realpath "^1.0.0"
116 | inflight "^1.0.4"
117 | inherits "2"
118 | minimatch "^3.0.4"
119 | once "^1.3.0"
120 | path-is-absolute "^1.0.0"
121 |
122 | https-proxy-agent@^2.2.1:
123 | version "2.2.2"
124 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793"
125 | integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==
126 | dependencies:
127 | agent-base "^4.3.0"
128 | debug "^3.1.0"
129 |
130 | inflight@^1.0.4:
131 | version "1.0.6"
132 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
133 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
134 | dependencies:
135 | once "^1.3.0"
136 | wrappy "1"
137 |
138 | inherits@2, inherits@^2.0.3, inherits@~2.0.3:
139 | version "2.0.4"
140 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
141 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
142 |
143 | isarray@~1.0.0:
144 | version "1.0.0"
145 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
146 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
147 |
148 | mime@^2.0.3:
149 | version "2.4.4"
150 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
151 | integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
152 |
153 | minimatch@^3.0.4:
154 | version "3.0.4"
155 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
156 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
157 | dependencies:
158 | brace-expansion "^1.1.7"
159 |
160 | minimist@0.0.8:
161 | version "0.0.8"
162 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
163 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
164 |
165 | mkdirp@0.5.1:
166 | version "0.5.1"
167 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
168 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
169 | dependencies:
170 | minimist "0.0.8"
171 |
172 | ms@2.0.0:
173 | version "2.0.0"
174 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
175 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
176 |
177 | ms@^2.1.1:
178 | version "2.1.2"
179 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
180 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
181 |
182 | once@^1.3.0:
183 | version "1.4.0"
184 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
185 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
186 | dependencies:
187 | wrappy "1"
188 |
189 | path-is-absolute@^1.0.0:
190 | version "1.0.1"
191 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
192 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
193 |
194 | pend@~1.2.0:
195 | version "1.2.0"
196 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
197 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
198 |
199 | process-nextick-args@~2.0.0:
200 | version "2.0.1"
201 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
202 | integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
203 |
204 | progress@^2.0.1:
205 | version "2.0.3"
206 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
207 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
208 |
209 | proxy-from-env@^1.0.0:
210 | version "1.0.0"
211 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
212 | integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
213 |
214 | puppeteer@1.20.0:
215 | version "1.20.0"
216 | resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38"
217 | integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ==
218 | dependencies:
219 | debug "^4.1.0"
220 | extract-zip "^1.6.6"
221 | https-proxy-agent "^2.2.1"
222 | mime "^2.0.3"
223 | progress "^2.0.1"
224 | proxy-from-env "^1.0.0"
225 | rimraf "^2.6.1"
226 | ws "^6.1.0"
227 |
228 | readable-stream@^2.2.2:
229 | version "2.3.6"
230 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
231 | integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
232 | dependencies:
233 | core-util-is "~1.0.0"
234 | inherits "~2.0.3"
235 | isarray "~1.0.0"
236 | process-nextick-args "~2.0.0"
237 | safe-buffer "~5.1.1"
238 | string_decoder "~1.1.1"
239 | util-deprecate "~1.0.1"
240 |
241 | rimraf@^2.6.1:
242 | version "2.7.1"
243 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
244 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
245 | dependencies:
246 | glob "^7.1.3"
247 |
248 | safe-buffer@~5.1.0, safe-buffer@~5.1.1:
249 | version "5.1.2"
250 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
251 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
252 |
253 | string_decoder@~1.1.1:
254 | version "1.1.1"
255 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
256 | integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
257 | dependencies:
258 | safe-buffer "~5.1.0"
259 |
260 | typedarray@^0.0.6:
261 | version "0.0.6"
262 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
263 | integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
264 |
265 | util-deprecate@~1.0.1:
266 | version "1.0.2"
267 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
268 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
269 |
270 | wrappy@1:
271 | version "1.0.2"
272 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
273 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
274 |
275 | ws@^6.1.0:
276 | version "6.2.1"
277 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
278 | integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
279 | dependencies:
280 | async-limiter "~1.0.0"
281 |
282 | yauzl@2.4.1:
283 | version "2.4.1"
284 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
285 | integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
286 | dependencies:
287 | fd-slicer "~1.0.1"
288 |
--------------------------------------------------------------------------------
/core/test/uix/compiler_test.cljs:
--------------------------------------------------------------------------------
1 | (ns uix.compiler-test
2 | (:require [clojure.test :refer [deftest is are testing run-tests]]
3 | [uix.compiler.alpha :as uixc]
4 | [uix.test-utils :refer [as-string js-equal? with-error symbol-for]]))
5 |
6 | (enable-console-print!)
7 |
8 | (deftest test-default-compare-args
9 | (is (uixc/*default-compare-args* #js {:argv 1} #js {:argv 1})))
10 |
11 | (deftest test-seq-return
12 | (let [ui #(for [x (range 2)]
13 | [:span x])]
14 | (is (= (as-string [ui]) "01"))))
15 |
16 | (when ^boolean goog.DEBUG
17 | (deftest test-default-format-display-name
18 | (is (= (uixc/default-format-display-name (.-name js-equal?))
19 | "uix.test-utils/js-equal?"))
20 | (let [f-hello (fn [])]
21 | (is (= (uixc/default-format-display-name (.-name f-hello))
22 | "f-hello"))))
23 |
24 | (deftest test-with-name
25 | (binding [uixc/*format-display-name* uixc/*format-display-name*]
26 | (let [f (fn
2 |
3 | _Idiomatic ClojureScript interface to modern React.js_
4 |
5 | Bug reports, feature requests and PRs are welcome 👌
6 |
7 | There are no versioned releases yet, use `deps.edn` to depend on the code via git deps.
8 |
9 | Core package, `react` wrapper.
10 |
--------------------------------------------------------------------------------
/dom/README.md:
--------------------------------------------------------------------------------
1 |