├── .nvmrc ├── .gitignore ├── logo.png ├── devtools.png ├── core ├── package.json ├── resources │ ├── public │ │ ├── logo.png │ │ ├── benchmark.html │ │ ├── index.html │ │ ├── example.html │ │ └── init.cljs │ └── externs.js ├── prod.cljs.edn ├── release.edn ├── .gitignore ├── example.cljs.edn ├── dev │ ├── uix │ │ ├── components.cljc │ │ ├── recipes │ │ │ ├── context.cljc │ │ │ ├── error_boundary.cljc │ │ │ ├── state_hook.cljc │ │ │ ├── popup.cljc │ │ │ ├── lazy_loading.cljc │ │ │ ├── interop.cljc │ │ │ ├── dynamic_styles.cljc │ │ │ ├── server_rendering.clj │ │ │ └── global_state.cljc │ │ ├── server.clj │ │ ├── recipes.cljc │ │ └── example.cljs │ └── user.clj ├── README.md ├── benchmark.cljs.edn ├── dev.cljs.edn ├── test │ └── uix │ │ ├── compiler │ │ └── alpha_test.clj │ │ ├── test_runner.clj │ │ ├── test_runner.cljs │ │ ├── test_utils.cljs │ │ ├── hooks_test.clj │ │ ├── xframe_test.cljc │ │ ├── aot_test.clj │ │ ├── core_test.clj │ │ ├── hooks_test.cljs │ │ ├── core_test.cljs │ │ ├── adapton_test.cljc │ │ ├── compiler_test.cljs │ │ └── ssr_test.cljc ├── src │ ├── uix │ │ ├── specs │ │ │ └── alpha.clj │ │ ├── compiler │ │ │ ├── aot.cljs │ │ │ └── alpha.cljs │ │ ├── lib.cljc │ │ ├── core │ │ │ ├── lazy_loader.cljc │ │ │ └── alpha.cljc │ │ └── hooks │ │ │ └── alpha.cljc │ └── xframe │ │ └── core │ │ ├── adapton.cljs │ │ ├── adapton.clj │ │ └── alpha.cljc ├── scripts │ ├── test │ ├── test-ci │ └── test.js ├── benchmark │ ├── uix │ │ ├── benchmark.cljs │ │ ├── benchmark.clj │ │ ├── react.cljs │ │ └── hiccup.cljs │ └── pages │ │ └── page1.html ├── pom.xml ├── deps.edn └── yarn.lock ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── rn ├── release.edn ├── src │ └── uix │ │ └── rn │ │ ├── alpha.clj │ │ └── alpha.cljs ├── deps.edn ├── setup │ ├── ios-core.cljs │ ├── android-core.cljs │ ├── ios-dev.cljs │ └── android-dev.cljs ├── pom.xml └── README.md ├── dom ├── release.edn ├── README.md ├── deps.edn ├── pom.xml └── src │ └── uix │ └── dom │ └── alpha.cljc ├── .circleci └── config.yml ├── README.md └── LICENSE.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .nrepl-port 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman01la/uix/master/logo.png -------------------------------------------------------------------------------- /devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman01la/uix/master/devtools.png -------------------------------------------------------------------------------- /core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "puppeteer": "1.20.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /core/resources/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roman01la/uix/master/core/resources/public/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: roman01la 4 | patreon: roman01la 5 | -------------------------------------------------------------------------------- /core/prod.cljs.edn: -------------------------------------------------------------------------------- 1 | {:static-fns true 2 | :fn-invoke-direct true 3 | :closure-defines {goog.DEBUG false} 4 | :externs ["resources/externs.js"]} 5 | -------------------------------------------------------------------------------- /rn/release.edn: -------------------------------------------------------------------------------- 1 | {:group-id "uix" 2 | :artifact-id "rn" 3 | :version "0.0.1-alpha" 4 | :skip-tag true 5 | :scm-url "https://github.com/roman01la/uix/tree/master/rn"} 6 | -------------------------------------------------------------------------------- /core/release.edn: -------------------------------------------------------------------------------- 1 | {:group-id "uix" 2 | :artifact-id "core" 3 | :version "0.0.1-alpha" 4 | :skip-tag true 5 | :scm-url "https://github.com/roman01la/uix/tree/master/core"} 6 | -------------------------------------------------------------------------------- /dom/release.edn: -------------------------------------------------------------------------------- 1 | {:group-id "uix" 2 | :artifact-id "dom" 3 | :version "0.0.1-alpha" 4 | :skip-tag true 5 | :scm-url "https://github.com/roman01la/uix/tree/master/dom"} 6 | -------------------------------------------------------------------------------- /core/resources/public/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cpcache 3 | resources/public/out 4 | resources/public/example-out 5 | resources/public/example.js 6 | resources/public/benchmark.js 7 | target 8 | .rebel_readline_history 9 | out 10 | index.html 11 | node_modules 12 | server_render_test 13 | -------------------------------------------------------------------------------- /core/example.cljs.edn: -------------------------------------------------------------------------------- 1 | {:output-to "resources/public/example.js" 2 | :output-dir "resources/public/example-out" 3 | :optimizations :simple 4 | :cache-analysis true 5 | :static-fns true 6 | :fn-invoke-direct true 7 | :optimize-constants true 8 | :verbose true 9 | :aot-cache false} 10 | -------------------------------------------------------------------------------- /core/dev/uix/components.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.components 2 | #?(:cljs (:require [cljs.loader :as loader]))) 3 | 4 | (defn alert [msg] 5 | [:ul {:style {:background "red" 6 | :color "white" 7 | :padding 8}} 8 | msg]) 9 | 10 | #?(:cljs (loader/set-loaded! :components)) 11 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | 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 | 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 | Rendering target package, `react-dom` and `react-dom/server` wrapper. 10 | -------------------------------------------------------------------------------- /core/benchmark.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:open-url "http://localhost:[[server-port]]/benchmark.html"} 2 | {:output-dir "resources/public/out" 3 | :output-to "resources/public/out/benchmark.js" 4 | :asset-path "out" 5 | :main uix.benchmark 6 | :static-fns true 7 | :fn-invoke-direct true 8 | :closure-defines {goog.DEBUG false} 9 | :externs ["resources/externs.js"]} 10 | -------------------------------------------------------------------------------- /core/dev.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:watch-dirs ["src" "dev"]} 2 | {:output-dir "resources/public/out" 3 | :asset-path "out" 4 | :modules {:recipes 5 | {:entries #{uix.recipes} 6 | :output-to "resources/public/out/recipes.js"} 7 | :components 8 | {:entries #{uix.components} 9 | :output-to "resources/public/out/components.js"}}} 10 | -------------------------------------------------------------------------------- /core/test/uix/compiler/alpha_test.clj: -------------------------------------------------------------------------------- 1 | (ns uix.compiler.alpha-test 2 | (:require [clojure.test :refer :all] 3 | [uix.compiler.alpha :as compiler])) 4 | 5 | (deftest test-62 6 | (is (= "
" 7 | (compiler/render-to-string [:div {:content-editable true}]))) 8 | (is (= "" 9 | (compiler/render-to-string [:div {:hidden true}])))) 10 | -------------------------------------------------------------------------------- /rn/src/uix/rn/alpha.clj: -------------------------------------------------------------------------------- 1 | (ns uix.rn.alpha 2 | (:require [clojure.string :as str])) 3 | 4 | (defn camel->dash [s] 5 | (-> (str/replace s #"[A-Z]" #(str "-" (str/lower-case %))) 6 | (subs 1))) 7 | 8 | (defmacro gen-adapted-components [module names] 9 | `(do 10 | ~@(for [name names] 11 | `(def ~(symbol (camel->dash name)) 12 | (adapt-fn (. ~module ~(symbol (str "-" name)))))))) 13 | -------------------------------------------------------------------------------- /core/src/uix/specs/alpha.clj: -------------------------------------------------------------------------------- 1 | (ns uix.specs.alpha 2 | (:require [clojure.spec.alpha :as s] 3 | [cljs.core.specs.alpha :as core.specs])) 4 | 5 | (s/def :lazy/libspec 6 | (s/and 7 | seq? 8 | (s/cat 9 | :quote #{'quote} 10 | :libspec (s/spec 11 | (s/cat 12 | :lib simple-symbol? 13 | :marker #{:refer} 14 | :refer ::core.specs/refer))))) 15 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/context.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.context 2 | (:require [uix.core.alpha :as uix.core :refer [defcontext]])) 3 | 4 | (defcontext *ctx*) 5 | 6 | (defn component [f] 7 | (let [v (uix.core/context *ctx*)] 8 | [:button {:on-click f} 9 | v])) 10 | 11 | (defn recipe [] 12 | (let [v (uix.core/state 0) 13 | f (uix.core/callback #(swap! v inc) [])] 14 | (uix.core/context-provider [*ctx* @v] 15 | [component f]))) 16 | -------------------------------------------------------------------------------- /core/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /core/test/uix/test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns uix.test-runner 2 | (:require [clojure.test :refer :all] 3 | [uix.core-test] 4 | [uix.compiler.alpha-test] 5 | [uix.hooks-test] 6 | [uix.ssr-test] 7 | [uix.xframe-test] 8 | [uix.aot-test])) 9 | 10 | (defn -main [& args] 11 | (run-tests 12 | #_'uix.ssr-test 13 | 'uix.core-test 14 | 'uix.compiler.alpha-test 15 | 'uix.hooks-test 16 | 'uix.aot-test 17 | 'uix.xframe-test)) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Include minimal repro** 14 | Code example or detailed explanation 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/error_boundary.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.error-boundary 2 | (:require [uix.core.alpha :as core])) 3 | 4 | (def error-boundary 5 | (core/create-error-boundary 6 | {:error->state (fn [error] {:error error}) 7 | :handle-catch (fn [error info] (println error))} 8 | (fn [state [child]] 9 | (let [{:keys [error]} @state] 10 | (if error 11 | error 12 | child))))) 13 | 14 | (defn erroring-component [] 15 | (throw "Hey!")) 16 | 17 | (defn recipe [] 18 | [error-boundary 19 | [erroring-component]]) 20 | -------------------------------------------------------------------------------- /rn/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.0"} 2 | org.clojure/clojurescript {:mvn/version "1.10.739"} 3 | uix.core/uix.core {:git/url "https://github.com/roman01la/uix.git" 4 | :deps/root "core" 5 | :sha "0da33eef38a7122be226b9b9a8ae0b5431b6b5d3"}} 6 | :paths ["src"] 7 | :aliases {:dev {:extra-deps {uix.core/uix.core {:local/root "../core"}}} 8 | :release {:extra-deps {appliedscience/deps-library {:mvn/version "0.3.4"}} 9 | :main-opts ["-m" "deps-library.release"]}}} 10 | -------------------------------------------------------------------------------- /core/dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [figwheel.main.api] 3 | [codox.main] 4 | [cljfmt.main] 5 | [clojure.java.io]) 6 | (:import (java.io File))) 7 | 8 | (defn start [] 9 | (figwheel.main.api/start "dev")) 10 | 11 | (defn docs [] 12 | (codox.main/generate-docs 13 | {:metadata {:doc/format :markdown} 14 | :themes [:rdash]})) 15 | 16 | (defn fmt [] 17 | (let [paths (->> ["src" "dev"] 18 | (map clojure.java.io/file) 19 | (filter #(and (.exists ^File %) (.isDirectory ^File %))))] 20 | (cljfmt.main/fix paths {:file-pattern #"\.clj[cs]?$"}))) 21 | -------------------------------------------------------------------------------- /dom/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.0"} 2 | org.clojure/clojurescript {:mvn/version "1.10.739"} 3 | cljsjs/react-dom {:mvn/version "17.0.2-0" 4 | :exclusions [cljsjs/react-dom-server]} 5 | uix.core/uix.core {:git/url "https://github.com/roman01la/uix.git" 6 | :deps/root "core" 7 | :sha "0da33eef38a7122be226b9b9a8ae0b5431b6b5d3"}} 8 | :paths ["src"] 9 | :aliases {:dev {:extra-deps {uix.core/uix.core {:local/root "../core"}}} 10 | :release {:extra-deps {appliedscience/deps-library {:mvn/version "0.3.4"}} 11 | :main-opts ["-m" "deps-library.release"]}}} 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/state_hook.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.state-hook 2 | "This recipe shows how to use local state in UIx components. 3 | 4 | `uix.core.alpha/state` takes initial value and returns atom-like 5 | reference type that schedules re-render when the value is updated. 6 | 7 | Note that update is applied asynchronous, which means that a new value 8 | might not be immediately available when dereferenced." 9 | (:require [uix.core.alpha :as uix])) 10 | 11 | (defn recipe [] 12 | (let [state* (uix/state {:value "Hello!"}) 13 | value* (uix/cursor-in state* [:value])] 14 | [:div 15 | [:input {:value @value* 16 | :on-change #(reset! value* (.. % -target -value))}] 17 | [:div "Input text: " @value*]])) 18 | -------------------------------------------------------------------------------- /core/test/uix/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.test-runner 2 | (:require [cljs.test] 3 | [uix.core-test] 4 | [uix.compiler-test] 5 | [uix.hooks-test] 6 | [uix.adapton-test] 7 | [uix.xframe-test])) 8 | 9 | (defmethod cljs.test/report [::cljs.test/default :summary] [m] 10 | (println "\nRan" (:test m) "tests containing" 11 | (+ (:pass m) (:fail m) (:error m)) "assertions.") 12 | (println (:fail m) "failures," (:error m) "errors.") 13 | (when (pos? (:fail m)) 14 | (js/testsFailed (:fail m))) 15 | (js/testsDone)) 16 | 17 | (cljs.test/run-tests 18 | (cljs.test/empty-env) 19 | 'uix.core-test 20 | 'uix.compiler-test 21 | 'uix.hooks-test 22 | 'uix.adapton-test 23 | 'uix.xframe-test) 24 | -------------------------------------------------------------------------------- /core/scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | echo "Installing NPM deps..." 7 | yarn install --frozen-lockfile 8 | 9 | echo "Running Clojure tests..." 10 | clojure -A:test -m uix.test-runner 11 | 12 | echo "
" > index.html 13 | 14 | echo "Building ClojureScript dev tests..." 15 | clojure -A:test -m cljs.main -c uix.test-runner 16 | 17 | echo "Running ClojureScript dev tests..." 18 | scripts/test.js $(pwd) 19 | 20 | echo "Building ClojureScript prod tests..." 21 | rm -rf out 22 | clojure -A:test -m cljs.main -O advanced \ 23 | -co '{:infer-externs true :closure-defines {goog.DEBUG false}}' \ 24 | -c uix.test-runner 25 | 26 | echo "Running ClojureScript prod tests..." 27 | scripts/test.js $(pwd) 28 | -------------------------------------------------------------------------------- /core/scripts/test-ci: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -o pipefail 5 | 6 | echo "Installing NPM deps..." 7 | yarn install --frozen-lockfile 8 | 9 | echo "Running Clojure tests..." 10 | clojure -A:test -m uix.test-runner 11 | 12 | echo "
" > index.html 13 | 14 | echo "Building ClojureScript dev tests..." 15 | clojure -A:test -m cljs.main -c uix.test-runner 16 | 17 | echo "Running ClojureScript dev tests..." 18 | scripts/test.js $(pwd) 19 | 20 | echo "Building ClojureScript prod tests..." 21 | rm -rf out 22 | clojure -A:test -m cljs.main -O advanced \ 23 | -co '{:infer-externs true :closure-defines {goog.DEBUG false}}' \ 24 | -c uix.test-runner 25 | 26 | echo "Running ClojureScript prod tests..." 27 | scripts/test.js $(pwd) 28 | -------------------------------------------------------------------------------- /core/scripts/test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const puppeteer = require('puppeteer'); 4 | 5 | (async function run() { 6 | let failures = 0; 7 | 8 | const browser = await puppeteer.launch(); 9 | const page = await browser.newPage(); 10 | 11 | page.on("console", m => { 12 | if (m.type() === "error") { 13 | console.error(`${m.text()} in ${m.location().url}:${m.location().lineNumber}`); 14 | } else { 15 | console.log(...m.args().map(a => a._remoteObject.value)); 16 | } 17 | }); 18 | 19 | await page.exposeFunction("testsFailed", n => { 20 | failures = n; 21 | } 22 | ); 23 | 24 | await page.exposeFunction("testsDone", async () => { 25 | await browser.close(); 26 | 27 | if (failures > 0) { 28 | process.exit(1); 29 | } 30 | } 31 | ); 32 | 33 | await page.goto(`file://${process.argv[2]}/index.html`); 34 | })(); 35 | -------------------------------------------------------------------------------- /core/test/uix/test_utils.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.test-utils 2 | (:require [uix.compiler.alpha :as uixc] 3 | ["react-dom/server" :as rserver] 4 | [goog.object :as gobj] 5 | [clojure.test :refer [is]] 6 | [uix.dom.alpha :as dom])) 7 | 8 | (defn as-string [comp] 9 | (-> (uixc/as-element comp) 10 | rserver/renderToStaticMarkup)) 11 | 12 | (defn js-equal? [a b] 13 | (gobj/equals a b)) 14 | 15 | (defn symbol-for [s] 16 | (js* "Symbol.for(~{})" s)) 17 | 18 | (defn react-element-of-type? [f type] 19 | (= (gobj/get f "$$typeof") (symbol-for type))) 20 | 21 | (defn with-error [f] 22 | (let [msgs (atom []) 23 | cc js/console.error] 24 | (set! js/console.error #(swap! msgs conj %)) 25 | (f) 26 | (set! js/console.error cc) 27 | (is (empty? @msgs)))) 28 | 29 | (defn render [el] 30 | (let [root (.createElement js/document "div")] 31 | (.append (.getElementById js/document "root") root) 32 | (dom/render el root))) 33 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/popup.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.popup 2 | "This recipe shows how to use React's portals 3 | to render components into a different DOM node." 4 | (:require [uix.core.alpha :as uix] 5 | [uix.dom.alpha :as dom])) 6 | 7 | (defn popup [{:keys [on-close]}] 8 | [:div {:style {:position :absolute 9 | :bottom 64 10 | :left 64 11 | :width 320 12 | :height 200 13 | :display :flex 14 | :justify-content :center 15 | :align-items :center 16 | :background "#d4e1ec" 17 | :box-shadow "0 2px 16px rgba(0, 0, 0, 0.05)"}} 18 | [:button {:on-click on-close} 19 | "Close"]]) 20 | 21 | (defn recipe [] 22 | (let [open?* (uix/state false)] 23 | [:<> 24 | [:button {:on-click #(reset! open?* true)} 25 | "Open popup"] 26 | #?(:cljs 27 | (when @open?* 28 | (dom/create-portal [popup {:on-close #(reset! open?* false)}] 29 | (.querySelector js/document "#popup-layer"))))])) 30 | -------------------------------------------------------------------------------- /rn/setup/ios-core.cljs: -------------------------------------------------------------------------------- 1 | (ns {{app-name}}.ios.core 2 | (:require [uix.core.alpha :as uix] 3 | [uix.rn.alpha :as n])) 4 | 5 | (def logo-img (n/require "./images/cljs.png")) 6 | 7 | (def styles 8 | (n/create-stylesheet 9 | {:screen {:flex-direction "column" :margin 40 :align-items "center"} 10 | :heading {:font-size 30 :font-weight "100" :margin-bottom 20 :text-align "center"} 11 | :logo {:width 80 :height 80 :margin-bottom 30} 12 | :button {:background-color "#999" :padding 10 :border-radius 5} 13 | :button-text {:color "white" :text-align "center" :font-weight "bold"}})) 14 | 15 | (defn app-root [] 16 | (let [greeting (uix/state "Hello!")] 17 | [n/view {:style (:screen styles)} 18 | [n/text {:style (:heading styles)} 19 | @greeting] 20 | [n/image {:source logo-img 21 | :style (:logo styles)}] 22 | [n/touchable-highlight {:style (:button styles) 23 | :on-press #(alert "HELLO!")} 24 | [n/text {:style (:button-text styles)} 25 | "press me"]]])) 26 | 27 | (defn init [] 28 | (n/register "{{app-name}}" #(uix/as-react app-root))) 29 | -------------------------------------------------------------------------------- /rn/setup/android-core.cljs: -------------------------------------------------------------------------------- 1 | (ns {{app-name}}.android.core 2 | (:require [uix.core.alpha :as uix] 3 | [uix.rn.alpha :as n])) 4 | 5 | (def logo-img (n/require "./images/cljs.png")) 6 | 7 | (def styles 8 | (n/create-stylesheet 9 | {:screen {:flex-direction "column" :margin 40 :align-items "center"} 10 | :heading {:font-size 30 :font-weight "100" :margin-bottom 20 :text-align "center"} 11 | :logo {:width 80 :height 80 :margin-bottom 30} 12 | :button {:background-color "#999" :padding 10 :border-radius 5} 13 | :button-text {:color "white" :text-align "center" :font-weight "bold"}})) 14 | 15 | (defn app-root [] 16 | (let [greeting (uix/state "Hello!")] 17 | [n/view {:style (:screen styles)} 18 | [n/text {:style (:heading styles)} 19 | @greeting] 20 | [n/image {:source logo-img 21 | :style (:logo styles)}] 22 | [n/touchable-highlight {:style (:button styles) 23 | :on-press #(alert "HELLO!")} 24 | [n/text {:style (:button-text styles)} 25 | "press me"]]])) 26 | 27 | (defn init [] 28 | (n/register "{{app-name}}" #(uix/as-react app-root))) 29 | -------------------------------------------------------------------------------- /core/test/uix/hooks_test.clj: -------------------------------------------------------------------------------- 1 | (ns uix.hooks-test 2 | (:require [clojure.test :refer :all] 3 | [uix.core.alpha :as core] 4 | [uix.hooks.alpha :as hooks])) 5 | 6 | (deftest test-state 7 | (is (= 1 @(core/state 1)))) 8 | 9 | (deftest test-cursor-in 10 | (is (= 1 @(core/cursor-in (core/state {:x 1}) [:x])))) 11 | 12 | (deftest test-ref 13 | (is (= nil @(core/ref))) 14 | (is (= 1 @(core/ref 1)))) 15 | 16 | (deftest test-with-deps-check 17 | (hooks/with-deps-check [prev-deps] 18 | (is (= [1] @prev-deps)) 19 | [1])) 20 | 21 | (deftest test-effect! 22 | (is (= nil (core/effect! identity)))) 23 | 24 | (deftest test-with-effect 25 | (is (= nil (core/with-effect [] (1))))) 26 | 27 | (deftest test-layout-effect! 28 | (is (= nil (core/layout-effect! identity)))) 29 | 30 | (deftest test-with-layout-effect 31 | (is (= nil (core/with-layout-effect [] (1))))) 32 | 33 | (deftest test-callback 34 | (is (= identity (core/callback identity)))) 35 | 36 | (deftest test-memo 37 | (is (= 1 (core/memo (constantly 1))))) 38 | 39 | (deftest test-reducer 40 | (let [[value dispatch] (core/reducer identity 1 inc)] 41 | (is (= 2 value)))) 42 | -------------------------------------------------------------------------------- /rn/setup/ios-dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load env.ios.main 2 | (:require [uix.core.alpha :as uix] 3 | [{{app-name}}.ios.core :as core] 4 | [figwheel.client :as fw] 5 | [env.config :as conf])) 6 | 7 | (enable-console-print!) 8 | 9 | (assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?") 10 | (assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?") 11 | 12 | (def cnt (atom 0)) 13 | 14 | (defn reloader [] 15 | (let [state (uix/state 0)] 16 | (uix/effect! 17 | (fn [] 18 | (add-watch cnt ::key #(swap! state inc)) 19 | #(remove-watch cnt ::key)) 20 | #js []) 21 | [core/app-root])) 22 | 23 | ;; Do not delete, root-el is used by the figwheel-bridge.js 24 | (def root-el (uix/as-element [reloader])) 25 | 26 | (defn force-reload! [] 27 | (swap! cnt inc)) 28 | 29 | (fw/start { 30 | :websocket-url (:ios conf/figwheel-urls) 31 | :heads-up-display false 32 | :jsload-callback force-reload!}) 33 | 34 | (core/init) 35 | -------------------------------------------------------------------------------- /rn/setup/android-dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load env.android.main 2 | (:require [uix.core.alpha :as uix] 3 | [{{app-name}}.android.core :as core] 4 | [figwheel.client :as fw] 5 | [env.config :as conf])) 6 | 7 | (enable-console-print!) 8 | 9 | (assert (exists? core/init) "Fatal Error - Your core.cljs file doesn't define an 'init' function!!! - Perhaps there was a compilation failure?") 10 | (assert (exists? core/app-root) "Fatal Error - Your core.cljs file doesn't define an 'app-root' function!!! - Perhaps there was a compilation failure?") 11 | 12 | (def cnt (atom 0)) 13 | 14 | (defn reloader [] 15 | (let [state (uix/state 0)] 16 | (uix/effect! 17 | (fn [] 18 | (add-watch cnt ::key #(swap! state inc)) 19 | #(remove-watch cnt ::key)) 20 | #js []) 21 | [core/app-root])) 22 | 23 | ;; Do not delete, root-el is used by the figwheel-bridge.js 24 | (def root-el (uix/as-element [reloader])) 25 | 26 | (defn force-reload! [] 27 | (swap! cnt inc)) 28 | 29 | (fw/start { 30 | :websocket-url (:android conf/figwheel-urls) 31 | :heads-up-display false 32 | :jsload-callback force-reload!}) 33 | 34 | (core/init) 35 | -------------------------------------------------------------------------------- /rn/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | uix 5 | rn 6 | 0.0.1-alpha 7 | rn 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.0 13 | 14 | 15 | org.clojure 16 | clojurescript 17 | 1.10.520 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | https://github.com/roman01la/uix/tree/master/rn 26 | 9836ce643b930fc2f996e57bc582168d18eb41ec 27 | scm:git:git@github.com:roman01la/uix.git 28 | scm:git:git@github.com:roman01la/uix.git 29 | 30 | 31 | -------------------------------------------------------------------------------- /core/src/uix/compiler/aot.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.compiler.aot 2 | "Runtime helpers for Hiccup compiled into React.js" 3 | (:require [react :as react] 4 | [uix.compiler.alpha :as r])) 5 | 6 | (def >el react/createElement) 7 | (def suspense react/Suspense) 8 | (def fragment react/Fragment) 9 | 10 | (defn fn-to-react-fn [f] 11 | (let [rf (fn -rf [props] 12 | (let [ret (apply f (.-argv props))] 13 | (if (vector? ret) 14 | (r/as-element ret) 15 | ret))) 16 | rf-memo (if-not ^boolean (.-uix-no-memo f) 17 | (react/memo rf r/*default-compare-args*) 18 | rf)] 19 | (when (and ^boolean goog.DEBUG (exists? js/__REACT_DEVTOOLS_GLOBAL_HOOK__)) 20 | (set! (.-uixf rf) f)) 21 | (when ^boolean goog.DEBUG 22 | (r/with-name f rf rf-memo)) 23 | (r/cache-react-fn f rf-memo) 24 | rf-memo)) 25 | 26 | (defn as-component [tag] 27 | (if-some [cached-fn (r/cached-react-fn tag)] 28 | cached-fn 29 | (fn-to-react-fn tag))) 30 | 31 | (defn component-element [tag attrs args] 32 | (let [js-props (or ^boolean attrs #js {}) 33 | el (as-component tag)] 34 | (set! (.-argv js-props) args) 35 | (>el el js-props))) 36 | -------------------------------------------------------------------------------- /core/src/uix/lib.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.lib 2 | #?(:cljs (:require-macros [uix.lib :refer [doseq-loop]]))) 3 | 4 | #?(:clj 5 | (defmacro doseq-loop [[v vs] & body] 6 | `(let [v# ~vs] 7 | (when (seq v#) 8 | (loop [x# (first v#) 9 | xs# (next v#)] 10 | (let [~v x#] 11 | ~@body) 12 | (when (seq xs#) 13 | (recur (first xs#) (next xs#)))))))) 14 | 15 | #?(:cljs 16 | (defn re-seq* 17 | "Similar to cljs.core/re-seq, but eager and faster" 18 | [re s] 19 | (loop [s s 20 | matches (.exec re s) 21 | ret #js []] 22 | (let [match-str (aget matches 0) 23 | match-vals (if (== (.-length matches) 1) 24 | match-str 25 | matches) 26 | post-idx (+ (.-index matches) (max 1 (.-length match-str))) 27 | next-s (subs s post-idx)] 28 | (.push ret match-vals) 29 | (if (<= post-idx (.-length s)) 30 | (if-some [next-matches (.exec re next-s)] 31 | (recur next-s next-matches ret) 32 | ret) 33 | ret))))) 34 | #?(:clj 35 | (defn cljs-env? [env] 36 | (boolean (:ns env)))) 37 | -------------------------------------------------------------------------------- /core/test/uix/xframe_test.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.xframe-test 2 | (:require [clojure.test :refer [deftest is testing run-tests]] 3 | [xframe.core.alpha :as xf])) 4 | 5 | (deftest test-memoize-last-by 6 | (let [calls-n (atom 0) 7 | f (fn [[x y]] 8 | (swap! calls-n inc) 9 | (+ x y)) 10 | f' (xf/memoize-last-by first next f)] 11 | (is (= 5 (f' 1 2 3))) 12 | (is (= 1 @calls-n)) 13 | (is (= 5 (f' 1 2 3))) 14 | (is (= 1 @calls-n)) 15 | (is (= 6 (f' 1 2 4))) 16 | (is (= 2 @calls-n)))) 17 | 18 | (deftest test-reg-sub 19 | 20 | (testing "db subscription" 21 | (reset! xf/db {:x 1}) 22 | (is (= {:x 1} (xf/<- [::xf/db])))) 23 | 24 | (testing "chain of subscriptions" 25 | (xf/reg-sub :db/x 26 | (fn [] 27 | (:x (xf/<- [::xf/db])))) 28 | (xf/reg-sub :db/x-as-string 29 | (fn [] 30 | (str (xf/<- [:db/x])))) 31 | (is (= "1" (xf/<- [:db/x-as-string])))) 32 | 33 | (testing "parameterized subscription" 34 | (reset! xf/db {:x [1 2 3 4]}) 35 | (xf/reg-sub :x/nth 36 | (fn [n] 37 | (-> (xf/<- [::xf/db]) :x (nth n)))) 38 | (is (= 1 (xf/<- [:x/nth 0]))) 39 | (is (= 3 (xf/<- [:x/nth 2]))))) 40 | 41 | (defn -main [] 42 | (run-tests)) 43 | -------------------------------------------------------------------------------- /core/benchmark/uix/benchmark.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.benchmark 2 | (:require-macros [uix.benchmark :refer [bench]]) 3 | (:require [reagent.core :as r] 4 | ["react-dom/server" :as rserver] 5 | [react :as react] 6 | [uix.compiler.alpha :as uixc] 7 | [uix.core.alpha :refer [html]] 8 | [uix.dom.alpha :as uix.dom] 9 | [uix.hiccup :as hiccup] 10 | [uix.react :refer [Editor]])) 11 | 12 | (defn reagent-interpret [] 13 | (r/as-element [hiccup/editor])) 14 | 15 | (defn uix-interpret [] 16 | (uixc/as-element [hiccup/editor])) 17 | 18 | (defn uix-compile [] 19 | (uixc/as-element [hiccup/editor-compiled])) 20 | 21 | 22 | (defn render [el] 23 | (rserver/renderToString el)) 24 | 25 | (do 26 | 27 | (bench :react 10000 (render (react/createElement Editor))) 28 | (bench :react 10000 (render (react/createElement Editor))) 29 | 30 | (bench :uix-compile 10000 (render (uix-compile))) 31 | (bench :uix-compile 10000 (render (uix-compile))) 32 | 33 | (bench :uix-interpret 10000 (render (uix-interpret))) 34 | (bench :uix-interpret 10000 (render (uix-interpret))) 35 | 36 | (bench :reagent-interpret 10000 (render (reagent-interpret))) 37 | (bench :reagent-interpret 10000 (render (reagent-interpret)))) 38 | 39 | (uix.dom/render [uix-interpret] js/root) 40 | 41 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/lazy_loading.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.lazy-loading 2 | "This recipe shows how to leverage React's Suspense and Closure's modules 3 | to load UIx components on-demand. 4 | 5 | First ClojureScript's compiler has to be instructed to emit modules, 6 | see `dev.cljs.edn` file. 7 | 8 | Entry point of every module have to report back to modules manager runtime 9 | when it's loaded using `cljs.loader/set-loaded!`. 10 | 11 | A module can require another module and refer to UIx component var in there 12 | using `require-lazy` macro that resembles Clojure's `require`. 13 | 14 | Referenced component should be put into `[:# {:fallback element} child]` form 15 | as a child element. This is a special Hiccup syntax for React.Suspense which 16 | takes care of UI tree while loading referenced component." 17 | (:require [uix.core.alpha :as uix] 18 | [uix.core.lazy-loader :refer [require-lazy]])) 19 | 20 | (require-lazy '[uix.components :refer [alert]]) 21 | 22 | (defn recipe [] 23 | (let [open?* (uix/state false)] 24 | [:<> 25 | [:button {:on-click #(reset! open?* true)} 26 | "Show alert"] 27 | [:div [:small "Throttle network in DevTools Network panel to see :fallback component"]] 28 | [:# {:fallback [:h2 "Loading uix.components/alert..."]} 29 | (when @open?* 30 | [alert "Lazy-loaded alert"])]])) 31 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/interop.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.interop 2 | "This recipe shows how JS React components can be used inside 3 | of UIx components and vice versa. 4 | 5 | In order to use JS component in UIx component there's a special 6 | Hiccup syntax `[:> component props & children]` where `props` is a map 7 | which will be transformed into JS Object with top-level keys camel-cased. 8 | Note that values are not touched, thus it's up to you to convert them 9 | before passing into JS component. 10 | 11 | UIx components can be adapted to JS React components 12 | using `uix.core.alpha/as-react`. It takes a function that takes 13 | JS props object transformed into immutable map. 14 | Again, values are not transformed." 15 | (:require #?(:cljs [react :as r]) 16 | #?(:cljs [goog.object :as gobj]) 17 | [uix.core.alpha :as uix])) 18 | 19 | #?(:cljs (def h r/createElement)) 20 | 21 | #?(:cljs 22 | (defn js-list [props] 23 | (let [items (gobj/get props "items") 24 | item (gobj/get props "itemComponent")] 25 | (h "ul" #js {} 26 | (map #(h item #js {:key %} %) items))))) 27 | 28 | (defn list-item [child] 29 | [:li child]) 30 | 31 | (def list-item* 32 | (uix/as-react #(list-item (:children %)))) 33 | 34 | (defn recipe [] 35 | #?(:cljs [:> js-list {:items #js [1 2 3] 36 | :item-component list-item*}])) 37 | -------------------------------------------------------------------------------- /core/dev/uix/server.clj: -------------------------------------------------------------------------------- 1 | (ns uix.server 2 | (:require [aleph.http :as http] 3 | [manifold.stream :as s] 4 | [uix.dom.alpha :as uix.dom] 5 | [uix.recipes :as recipes] 6 | [clojure.string :as str])) 7 | 8 | (def head 9 | " 10 | 11 | 12 | 13 | 14 | 15 |
") 16 | 17 | (def end 18 | "
19 |
20 | 21 | 22 | 23 | ") 24 | 25 | (defn response [] 26 | (let [res (s/stream)] 27 | (future 28 | (s/put! res head) 29 | (uix.dom/render-to-stream [recipes/root] {:on-chunk #(s/put! res %)}) 30 | (s/put! res end) 31 | (s/close! res)) 32 | {:status 200 33 | :headers {"content-type" "text/html"} 34 | :body res})) 35 | 36 | (defn handler [req] 37 | (cond 38 | (= (:uri req) "/") 39 | (response) 40 | 41 | (str/starts-with? (:uri req) "/out/") 42 | {:status 200 43 | :headers {"content-type" "application/javascript"} 44 | :body (slurp (str "resources/public" (:uri req)))} 45 | 46 | :else {:status 404 47 | :headers {"content-type" "text/plain"} 48 | :body "404"})) 49 | 50 | (defn -main [] 51 | (let [port 8080] 52 | (http/start-server #'handler {:port port}) 53 | (println (str "Server started at http://localhost:" port)))) 54 | -------------------------------------------------------------------------------- /dom/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | uix 5 | dom 6 | 0.0.1-alpha 7 | dom 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.0 13 | 14 | 15 | org.clojure 16 | clojurescript 17 | 1.10.520 18 | 19 | 20 | cljsjs 21 | react-dom 22 | 16.9.0-0 23 | 24 | 25 | cljsjs 26 | react-dom-server 27 | 28 | 29 | 30 | 31 | 32 | src 33 | 34 | 35 | 36 | https://github.com/roman01la/uix/tree/master/dom 37 | 20814de32444706375f8272c9dc0c6de935b763d 38 | scm:git:git@github.com:roman01la/uix.git 39 | scm:git:git@github.com:roman01la/uix.git 40 | 41 | 42 | -------------------------------------------------------------------------------- /core/dev/uix/recipes.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes 2 | (:require [uix.recipes.dynamic-styles :as dynamic-styles] 3 | [uix.recipes.lazy-loading :as lazy-loading] 4 | [uix.recipes.state-hook :as state-hook] 5 | [uix.recipes.global-state :as global-state] 6 | [uix.recipes.popup :as popup] 7 | [uix.recipes.interop :as interop] 8 | [uix.recipes.error-boundary :as error-boundary] 9 | [uix.recipes.context :as context] 10 | [uix.core.alpha :as uix] 11 | [uix.dom.alpha :as uix.dom] 12 | #?(:cljs [cljs.loader :as loader]))) 13 | 14 | (def recipes 15 | {:dynamic-styles dynamic-styles/recipe 16 | :lazy-loading lazy-loading/recipe 17 | :state-hook state-hook/recipe 18 | :global-state global-state/recipe 19 | :popup popup/recipe 20 | :interop interop/recipe 21 | :error-boundary error-boundary/recipe 22 | :context context/recipe}) 23 | 24 | (defn root [] 25 | (let [current-recipe* (uix/state :state-hook)] 26 | [:div {:style {:padding 24}} 27 | [:div {:style {:margin-bottom 16}} 28 | [:span "Select recipe: "] 29 | [:select {:value @current-recipe* 30 | :on-change #(reset! current-recipe* (keyword (.. % -target -value)))} 31 | (for [[k v] recipes] 32 | ^{:key k} 33 | [:option {:value k} 34 | (name k)])]] 35 | (when-let [recipe (get recipes @current-recipe*)] 36 | [:div 37 | [recipe]])])) 38 | 39 | #?(:cljs (uix.dom/hydrate [root] js/root)) 40 | 41 | #?(:cljs (loader/set-loaded! :recipes)) 42 | -------------------------------------------------------------------------------- /core/resources/public/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | UIx — Idiomatic ClojureScript interface to modern React.js 5 | 6 | 7 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/dynamic_styles.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.dynamic-styles 2 | "This recipe shows how to use UIx's API to hook into Hiccup transformation, 3 | in particular here it's used to transform inline CSS with CSS-in-JS library. 4 | 5 | `uix.core.alpha/add-transform-fn` takes a function that takes a map 6 | of attributes of Hiccup form when it is being transformed and returns 7 | the same map." 8 | (:require [uix.core.alpha :as uix] 9 | #?(:cljs [cljsjs.emotion]))) 10 | 11 | #?(:cljs 12 | (defonce _init-css-attr-transform 13 | (do 14 | (uix/add-transform-fn 15 | (fn [attrs] 16 | (if-not (contains? attrs :css) 17 | attrs 18 | (let [classes (:class attrs) 19 | css (:css attrs) 20 | class (->> (clj->js css) 21 | js/emotion.css 22 | (str classes " "))] 23 | (-> attrs 24 | (dissoc :css) 25 | (assoc :class class)))))) 26 | 0))) 27 | 28 | (defn recipe [] 29 | (let [border-color* (uix/state "#000")] 30 | [:<> 31 | [:div "Change border color (red, blue, etc.)"] 32 | [:input {:value @border-color* 33 | :on-change #(reset! border-color* (.. % -target -value)) 34 | :css {:border-width 3 35 | :border-color @border-color* 36 | :border-style :solid 37 | :border-radius 5 38 | :padding "4px 12px" 39 | :font-size 14 40 | :outline :none}}]])) 41 | -------------------------------------------------------------------------------- /core/src/uix/core/lazy_loader.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.core.lazy-loader 2 | #?(:cljs (:require-macros [uix.core.lazy-loader])) 3 | #?(:clj (:require [clojure.spec.alpha :as s] 4 | [uix.specs.alpha] 5 | [uix.lib]) 6 | :cljs (:require [cljs.loader] 7 | [react]))) 8 | 9 | #?(:cljs 10 | (def react-lazy react/lazy)) 11 | 12 | #?(:cljs 13 | (def load! cljs.loader/load)) 14 | 15 | #?(:clj 16 | (s/fdef require-lazy 17 | :args (s/cat :form :lazy/libspec))) 18 | 19 | #?(:clj 20 | (defmacro require-lazy 21 | "require-like macro, returns lazy-loaded React components. 22 | 23 | (require-lazy '[my.ns.components :refer [c1 c2]])" 24 | [form] 25 | (if-not (uix.lib/cljs-env? &env) 26 | `(clojure.core/require ~form) 27 | (let [m (s/conform :lazy/libspec form)] 28 | (when (not= m :clojure.spec.alpha/invalid) 29 | (let [{:keys [lib refer]} (:libspec m) 30 | module (->> (str lib) 31 | (re-find #"\.([a-z0-9-]+)") 32 | second 33 | keyword)] 34 | `(do 35 | ~@(for [sym refer] 36 | (let [qualified-sym (symbol (str lib "/" sym)) 37 | as-lazy `(uix.compiler.alpha/as-lazy-component (deref (cljs.core/resolve '~qualified-sym))) 38 | export `(cljs.core/js-obj "default" ~as-lazy) 39 | on-load `(fn [ok# fail#] (load! ~module #(ok# ~export)))] 40 | `(def ~sym (react-lazy (fn [] (~'js/Promise. ~on-load))))))))))))) 41 | -------------------------------------------------------------------------------- /rn/README.md: -------------------------------------------------------------------------------- 1 | 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 | Rendering target package, `react-native` wrapper. 10 | 11 | ## How to use UIx in React Native 12 | 13 | 1. Install [re-natal](https://github.com/drapanjanas/re-natal) 14 | 2. Initialize Reagent-based project `re-natal init {{AppName}}` 15 | 3. Remove `reagent` from `:dependencies` in `project.clj` 16 | 4. Add `[lein-tools-deps "0.4.5"]` to `:plugins` in `project.clj` 17 | 5. Add `:middleware [lein-tools-deps.plugin/resolve-dependencies-with-deps-edn]` to `project.clj` 18 | 6. Add `:lein-tools-deps/config {:config-files [:project] :clojure-executables ["/usr/local/bin/clojure"]}` to `project.clj` 19 | 7. Replace `env/dev/env/ios/main.cljs` with contents of `setup/ios-dev.cljs`, and replace `{{app-name}}` with your's apps root ns name 20 | 8. Replace `env/dev/env/android/main.cljs` with contents of `setup/android-dev.cljs`, and replace `{{app-name}}` with your's apps root ns name 21 | 9. Delete `src/reagent` directory 22 | 10. Delete `src/{{app-name}}/db.cljs`, `src/{{app-name}}/subs.cljs` and `src/{{app-name}}/events.cljs` files 23 | 11. Replace `src/{{app-name}}/ios/core.cljs` with contents of `setup/ios-core.cljs`, and replace `{{app-name}}` with your's apps root ns name 24 | 12. Replace `src/{{app-name}}/android/core.cljs` with contents of `setup/android-core.cljs`, and replace `{{app-name}}` with your's apps root ns name 25 | 13. Follow [re-natal's README](https://github.com/drapanjanas/re-natal) for further instructions 26 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | uix 5 | core 6 | 0.0.1-alpha 7 | core 8 | 9 | 10 | org.clojure 11 | clojure 12 | 1.10.1 13 | 14 | 15 | org.clojure 16 | clojurescript 17 | 1.10.597 18 | 19 | 20 | cljs-bean 21 | cljs-bean 22 | 1.4.0 23 | 24 | 25 | cljsjs 26 | react 27 | 16.9.0-0 28 | 29 | 30 | cljsjs 31 | react-dom-server 32 | 33 | 34 | 35 | 36 | 37 | src 38 | 39 | 40 | 41 | https://github.com/roman01la/uix/tree/master/core 42 | f8e8302bc460ba76fb1db648b4f6ece013fedb5e 43 | scm:git:git@github.com:roman01la/uix.git 44 | scm:git:git@github.com:roman01la/uix.git 45 | 46 | 47 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/clojure:tools-deps-1.10.0.442-node 7 | command: "/bin/bash" 8 | 9 | working_directory: ~/repo 10 | 11 | steps: 12 | - checkout 13 | 14 | - run: 15 | name: Install Headless Chrome dependencies 16 | command: | 17 | sudo apt-get update && \ 18 | sudo apt-get install -yq \ 19 | gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ 20 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ 21 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \ 22 | libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \ 23 | fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget 24 | 25 | - restore_cache: 26 | keys: 27 | - v1-dependencies-{{ checksum "core/deps.edn" }} 28 | - v1-dependencies- 29 | 30 | - restore_cache: 31 | keys: 32 | - v1-npm-deps-{{ checksum "core/yarn.lock" }} 33 | - v1-npm-deps- 34 | 35 | - run: cd core && yarn install --frozen-lockfile 36 | 37 | - run: cd core && scripts/test-ci 38 | 39 | - save_cache: 40 | paths: 41 | - ~/.m2 42 | key: v1-dependencies-{{ checksum "core/deps.edn" }} 43 | 44 | - save_cache: 45 | paths: 46 | - ~/.cache/yarn 47 | key: v1-npm-deps-{{ checksum "core/yarn.lock" }} 48 | 49 | workflows: 50 | version: 2 51 | test: 52 | jobs: 53 | - build 54 | -------------------------------------------------------------------------------- /core/benchmark/pages/page1.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 10 |
11 |

2016

12 |

The Blessing of Interactive Development 4/11 13 |

2015

14 |

The Web After Tomorrow 6/23 15 |

A shallow dive into DataScript internals 2/23 16 |

2014

17 |

Couple of DataScript resources 12/18 18 |

Streams: Mail 3.0 concept 10/27 19 |

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

Chatting cats use DataScript for fun 9/18 21 |

Irrelevant Things 8/12 22 |

Reinventing Git interface 6/17 23 |

Unofficial guide to Datomic internals 5/6 24 |

Datomic as a Protocol 4/29 25 |

Decomposing web app development 4/24 26 | 27 | 28 | -------------------------------------------------------------------------------- /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 | "Page 30 | 34 |

") 35 | 36 | (def end 37 | "
") 38 | 39 | (defn non-chunked-response 40 | "Client starts receiving response immediately 41 | but actual content starts download after 42 | rendering is done, ~500ms" 43 | [] 44 | (let [res (s/stream)] 45 | (future 46 | (s/put! res head) 47 | (s/put! res (uix.dom/render-to-string [app 4])) 48 | (s/put! res end) 49 | (s/close! res)) 50 | {:status 200 51 | :headers {"content-type" "text/html"} 52 | :body res})) 53 | 54 | (defn chunked-response 55 | "Client immediately starts receiving response and rendering content" 56 | [] 57 | (let [res (s/stream)] 58 | (future 59 | (s/put! res head) 60 | (uix.dom/render-to-stream [app 4] {:on-chunk #(s/put! res %)}) 61 | (s/put! res end) 62 | (s/close! res)) 63 | {:status 200 64 | :headers {"content-type" "text/html"} 65 | :body res})) 66 | 67 | (defn handler [req] 68 | (case (:uri req) 69 | "/" (non-chunked-response) 70 | "/chunked" (chunked-response) 71 | {:status 404 72 | :headers {"content-type" "text/plain"} 73 | :body "404"})) 74 | 75 | (defn -main [] 76 | (http/start-server #'handler {:port 8080}) 77 | (println 78 | " 79 | Server started at http://localhost:8080 80 | 81 | Visit http://localhost:8080 for non-chunked rendering example 82 | Visit http://localhost:8080/chunked for chunked rendering example")) 83 | 84 | (comment 85 | (def server 86 | (http/start-server #'handler {:port 8080})) 87 | 88 | (defn stop! [] 89 | (.close server))) 90 | -------------------------------------------------------------------------------- /core/test/uix/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns uix.core-test 2 | (:require [clojure.test :refer :all] 3 | [uix.core.alpha :as core] 4 | [uix.compiler.alpha :as compiler] 5 | [uix.core.lazy-loader :refer [require-lazy]])) 6 | 7 | (deftest test-strict-mode 8 | (is (= 1 (core/strict-mode 1)))) 9 | 10 | (deftest test-create-ref 11 | (is (= 1 @(core/create-ref 1)))) 12 | 13 | (deftest test-memoize 14 | (is (= identity (core/memoize identity)))) 15 | 16 | (deftest test-as-element 17 | (is (= 1 (core/as-element 1)))) 18 | 19 | (deftest test-as-react 20 | (is (= identity (core/as-react identity)))) 21 | 22 | (require-lazy '[clojure.string :refer [blank?]]) 23 | 24 | (deftest test-require-lazy 25 | (testing "Should refer a var from ns" 26 | (is (= blank? clojure.string/blank?))) 27 | 28 | (testing "Should fail to alias ns" 29 | (try 30 | (macroexpand-1 '(require-lazy '[clojure.string :as str])) 31 | (catch Exception e 32 | (is (some? e)))))) 33 | 34 | (deftest test-defui 35 | (core/defui hello [x] x) 36 | (is (fn? hello)) 37 | (is (= 1 (hello 1)))) 38 | 39 | (deftest test-create-error-boundary 40 | 41 | (testing "No error in error boundary" 42 | (let [error->state-called? (atom false) 43 | handle-catch-called? (atom false) 44 | err-b (core/create-error-boundary 45 | {:error->state #(reset! error->state-called? true) 46 | :handle-catch #(reset! handle-catch-called? true)} 47 | (fn [err args] 48 | (is (= nil @err)) 49 | (is (= [1] args))))] 50 | 51 | (compiler/render-to-static-markup [err-b 1]) 52 | (is (= false @error->state-called?)) 53 | (is (= false @handle-catch-called?)))) 54 | 55 | (testing "Error in error boundary" 56 | (let [handle-catch (atom nil) 57 | child (fn [] (throw (Exception. "Hello!"))) 58 | err-b (core/create-error-boundary 59 | {:error->state ex-message 60 | :handle-catch (fn [err info] 61 | (println (ex-message err) info) 62 | (reset! handle-catch err))} 63 | (fn [cause [x child]] 64 | (is (= 1 x)) 65 | (if (nil? @cause) 66 | child 67 | (is (= "Hello!" @cause)))))] 68 | 69 | (compiler/render-to-static-markup [err-b 1 [child]]) 70 | (is (some? @handle-catch))))) 71 | 72 | (deftest test-ifn-component 73 | (defmulti multi-c (fn [t _] t)) 74 | (defmethod multi-c :div [_ child] 75 | [:div child]) 76 | (defmethod multi-c :span [_ child] 77 | [:span child]) 78 | (is (= "
1
" (compiler/render-to-static-markup [multi-c :div 1]))) 79 | (is (= "1" (compiler/render-to-static-markup [multi-c :span 1])))) 80 | 81 | (deftest test-context 82 | (core/defcontext *ctx* 0) 83 | (is (== *ctx* 0)) 84 | (core/context-provider [*ctx* 1] 85 | (is (== *ctx* 1))) 86 | (is (== *ctx* 0))) 87 | 88 | (deftest test-context-component 89 | (core/defcontext *comp-ctx*) 90 | (let [foo (fn [] [:div (core/context *comp-ctx*)]) 91 | bar (fn [] (core/context-provider [*comp-ctx* 1] [foo]))] 92 | (is (= "
1
" 93 | (compiler/render-to-string [bar]))))) 94 | -------------------------------------------------------------------------------- /core/test/uix/hooks_test.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.hooks-test 2 | (:require [clojure.test :refer [deftest is testing run-tests async]] 3 | [uix.hooks.alpha :as hooks :refer [maybe-js-deps]] 4 | [uix.core.alpha :as core] 5 | [uix.test-utils :as t])) 6 | 7 | (deftest test-maybe-js-deps 8 | (is (.isArray js/Array (maybe-js-deps []))) 9 | (is (= (maybe-js-deps nil) js/undefined))) 10 | 11 | (deftest test-state-hook 12 | (let [f-state (fn [done] 13 | (let [state (core/state 1)] 14 | (is (instance? hooks/StateHook state)) 15 | (is (or (== @state 1) (== @state 2))) 16 | (if (== @state 2) 17 | (done) 18 | (swap! state inc))))] 19 | (async done 20 | (t/render [f-state done])))) 21 | 22 | (deftest test-reducer-hook 23 | (let [f-state (fn [done] 24 | (let [[value dispatch] (core/reducer (fn [value event] (inc value)) 1 inc)] 25 | (is (or (== value 2) (== value 3))) 26 | (if (== value 3) 27 | (done) 28 | (dispatch [:inc]))))] 29 | (async done 30 | (t/render [f-state done])))) 31 | 32 | (deftest test-cursor-in-hook 33 | (let [f-state (fn [done] 34 | (let [state (core/state {:x 1}) 35 | x (core/cursor-in state [:x])] 36 | (is (instance? hooks/Cursor x)) 37 | (is (or (== @x 1) (== @x 2))) 38 | (if (== @x 2) 39 | (done) 40 | (swap! x inc))))] 41 | (async done 42 | (t/render [f-state done])))) 43 | 44 | (deftest test-state-hook-identity 45 | (let [f-state (fn [done] 46 | (let [xs (core/state [])] 47 | (if (< (count @xs) 2) 48 | (swap! xs conj xs) 49 | (let [[s1 s2] @xs] 50 | (is (identical? s1 s2)) 51 | (done)))))] 52 | (async done 53 | (t/render [f-state done])))) 54 | 55 | (deftest test-ref-hook-mutable 56 | (let [f-ref (fn [done] 57 | (let [ref (core/ref 1)] 58 | (is (instance? hooks/RefHook ref)) 59 | (is (== @ref 1)) 60 | (swap! ref inc) 61 | (is (== @ref 2)) 62 | (done)))] 63 | (async done 64 | (t/render [f-ref done])))) 65 | 66 | (deftest test-ref-hook-memoized-instance 67 | (let [f-ref (fn [done] 68 | (let [ref (core/ref 1) 69 | refs (core/state [])] 70 | (if (< (count @refs) 2) 71 | (swap! refs conj ref) 72 | (let [[r1 r2] @refs] 73 | (is (identical? r1 r2)) 74 | (done)))))] 75 | (async done 76 | (t/render [f-ref done])))) 77 | 78 | #_(deftest test-effect-hook 79 | (let [f-effect (fn [done] 80 | (let [calls (core/state 0)] 81 | (core/effect! 82 | (fn [] 83 | (if (== 0 @calls) 84 | (swap! calls inc) 85 | (do (is (== @calls 1)) 86 | (done))))) 87 | @calls))] 88 | (async done 89 | (render [f-effect done])))) 90 | 91 | (defn -main [] 92 | (run-tests)) 93 | 94 | -------------------------------------------------------------------------------- /core/benchmark/uix/benchmark.clj: -------------------------------------------------------------------------------- 1 | (ns uix.benchmark 2 | (:require [clojure.string :as str] 3 | [net.cgrand.enlive-html :as enlive] 4 | [clojure.test :refer [deftest]] 5 | [criterium.core :as criterium] 6 | [hiccup.core :as hiccup] 7 | [rum.core :as rum] 8 | [uix.dom.alpha :as uix.dom]) 9 | (:import (java.io ByteArrayInputStream))) 10 | 11 | (defmacro bench [k iters expr] 12 | `(let [start# (js/Date.now) 13 | ret# (dotimes [_# ~iters] ~expr) 14 | end# (js/Date.now) 15 | elapsed# (- end# start#) 16 | rate# (js/Math.round (* (/ ~iters elapsed#) 1000))] 17 | (println (str ~(name k) " x " rate# " ops/s, elapsed " elapsed# "ms")))) 18 | 19 | 20 | ;; ======================== 21 | 22 | (def ^:dynamic *convert-style?* true) 23 | 24 | (defn convert-tag-name [tag attrs] 25 | (let [id (:id attrs) 26 | classes (when-not (str/blank? (:class attrs)) 27 | (->> (str/split (:class attrs) #"\s+") 28 | (remove str/blank?)))] 29 | (keyword 30 | (str tag 31 | (when id (str "#" id)) 32 | (when-not (empty? classes) 33 | (str "." (str/join "." classes))))))) 34 | 35 | (defn convert-style [s] 36 | (into {} 37 | (for [[_ k v] (re-seq #"([\w+\-]+)\s*:\s*([^;]+)" s)] 38 | (let [k' (keyword k) 39 | v' (condp re-matches v 40 | #"(\d+)px" :>> (fn [[_ n]] (Long/parseLong n)) 41 | #"(\d+\.\d+)px" :>> (fn [[_ n]] (Double/parseDouble n)) 42 | v)] 43 | [k' v'])))) 44 | 45 | (defn convert-attrs [attrs] 46 | (cond-> attrs 47 | true (dissoc :class :id :data-bem) 48 | (and *convert-style?* 49 | (contains? attrs :style)) (update :style convert-style) 50 | true not-empty)) 51 | 52 | (defn convert-tag [form] 53 | (cond 54 | ;; tag 55 | (map? form) 56 | (if (= :comment (:type form)) 57 | nil 58 | (let [{:keys [tag attrs content type]} form 59 | tag' (convert-tag-name (name tag) attrs) 60 | attrs' (convert-attrs attrs) 61 | children (->> (map convert-tag content) 62 | (remove nil?))] 63 | (vec 64 | (concat [tag'] (when attrs' [attrs']) children)))) 65 | 66 | ;; text node 67 | (string? form) 68 | (if (str/blank? form) nil form))) 69 | 70 | (defn convert-page [page] 71 | (-> (slurp page) 72 | .getBytes 73 | ByteArrayInputStream. 74 | enlive/html-resource 75 | (enlive/select [:body]) 76 | first 77 | convert-tag)) 78 | 79 | (defn -main [& args] 80 | (doseq [page ["page1.html" 81 | "page2.html" 82 | "page3.html"] 83 | :let [path (str "benchmark/pages/" page)]] 84 | (let [comp (convert-page path)] 85 | (println "\n--- Rum: testing" page "---") 86 | (criterium/quick-bench (rum/render-static-markup comp)) 87 | 88 | (println "\n--- UIx: testing" page "---") 89 | (criterium/quick-bench (uix.dom/render-to-static-markup comp)) 90 | 91 | (println "\n--- UIx streaming: testing" page "---") 92 | (criterium/quick-bench (uix.dom/render-to-static-stream comp {:on-chunk (fn [_])}))) 93 | 94 | (let [comp (binding [*convert-style?* false] 95 | (convert-page path))] 96 | (println "\n+++ With Hiccup +++") 97 | (criterium/quick-bench (hiccup/html comp))))) 98 | -------------------------------------------------------------------------------- /core/src/xframe/core/adapton.clj: -------------------------------------------------------------------------------- 1 | (ns xframe.core.adapton 2 | (:require [uix.lib])) 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 [^:volatile-mutable thunk 18 | ^:volatile-mutable result 19 | ^:volatile-mutable sub 20 | ^:volatile-mutable sup 21 | ^:volatile-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 | (set! sub (conj sub a-sub)) 32 | (set-sup! a-sub (conj (get-sup a-sub) this))) 33 | (-edge! [this a-sub] 34 | (set! sub (disj sub a-sub)) 35 | (set-sup! a-sub (disj (get-sup a-sub) this))) 36 | (compute [this] 37 | (if clean? 38 | result 39 | (do 40 | (run! #(-edge! this %) sub) 41 | (set! clean? true) 42 | (set! result (thunk)) 43 | (recur)))) 44 | (dirty! [this] 45 | (when clean? 46 | (set! clean? false) 47 | (run! dirty! sup))) 48 | (set-thunk! [this new-thunk] 49 | (set! thunk new-thunk)) 50 | (set-result! [this new-result] 51 | (set! result new-result)) 52 | 53 | clojure.lang.IDeref 54 | (deref [this] 55 | (let [prev-adapting (volatile! @curr-adapting) 56 | _ (vreset! curr-adapting this) 57 | result (compute this) 58 | _ (vreset! curr-adapting @prev-adapting)] 59 | (when @curr-adapting 60 | (+edge! @curr-adapting this)) 61 | result)) 62 | 63 | clojure.lang.IAtom 64 | (reset [this v] 65 | (set-result! this v) 66 | (dirty! this) 67 | result) 68 | 69 | (swap [this f] 70 | (.reset this (f (.deref this)))) 71 | (swap [this f x] 72 | (.reset this (f (.deref this) x))) 73 | (swap [this f x y] 74 | (.reset this (f (.deref this) x y))) 75 | (swap [this f x y args] 76 | (.reset this (apply f (.deref this) x y args))) 77 | 78 | clojure.lang.IMeta 79 | (meta [this] 80 | ameta)) 81 | 82 | (defn adapton? [v] 83 | (instance? Adapton v)) 84 | 85 | (defn make-athunk [thunk & [meta]] 86 | (Adapton. thunk nil #{} #{} false meta)) 87 | 88 | (defn aref [v] 89 | (let [a (Adapton. nil v #{} #{} true nil)] 90 | (set-thunk! a #(get-result a)) 91 | a)) 92 | 93 | (defmacro adapt [e & [meta]] 94 | `(make-athunk (fn [] ~e) ~meta)) 95 | 96 | (defn avar-get [v] 97 | (deref (deref v))) 98 | 99 | (defmacro amemo [args & body] 100 | (let [argsym (gensym) 101 | m (assoc (meta args) :args argsym) 102 | mexpr (if (uix.lib/cljs-env? &env) 103 | `(when ~'^boolean goog.DEBUG ~m) 104 | m)] 105 | `(let [f# (fn ~args ~@body) 106 | f*# (memoize (fn [~argsym] (adapt (apply f# ~argsym) ~mexpr)))] 107 | (fn [& args#] @(f*# args#))))) 108 | 109 | (defmacro xf-amemo [args & body] 110 | (let [argsym (gensym) 111 | m (assoc (meta args) :args argsym) 112 | mexpr (if (uix.lib/cljs-env? &env) 113 | `(when ~'^boolean goog.DEBUG ~m) 114 | m)] 115 | `(let [f# (fn ~args ~@body) 116 | f*# (xframe.core.alpha/memoize-last-by first second 117 | (fn [~argsym] (adapt (apply f# ~argsym) ~mexpr)))] 118 | (fn [key# args#] @(f*# key# args#))))) 119 | 120 | (defmacro defamemo [f args & body] 121 | `(def ~f (amemo ~args ~@body))) 122 | 123 | (defmacro avar [e] 124 | `(aref (adapt ~e))) 125 | 126 | (defmacro defavar [name e] 127 | `(def ~name (avar ~e))) 128 | 129 | (defmacro avar-set! [v e] 130 | `(reset! ~v (adapt ~e))) 131 | -------------------------------------------------------------------------------- /rn/src/uix/rn/alpha.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.rn.alpha 2 | "Public API" 3 | (:refer-clojure :exclude [require]) 4 | (:require [uix.core.alpha :as uix] 5 | [uix.compiler.alpha :as compiler] 6 | [react-native :as rn] 7 | [goog.object :as gobj])) 8 | 9 | (def ReactNative (js/require "react-native")) 10 | 11 | 12 | ;; Helpers 13 | (defn require [path] 14 | (js/require path)) 15 | 16 | (defn adapt-fn [f] 17 | (fn [props & children] 18 | (into [:> f props] children))) 19 | 20 | 21 | ;; react-native top-level API 22 | 23 | 24 | ;; Registry 25 | (def registry (.-AppRegistry ReactNative)) 26 | 27 | (defn register [app-name callback] 28 | (.registerComponent registry app-name callback)) 29 | 30 | 31 | ;; APIs 32 | (def AccessibilityInfo (.-AccessibilityInfo ReactNative)) 33 | (def ActionSheetIOS (.-ActionSheetIOS ReactNative)) 34 | (def Alert (.-Alert ReactNative)) 35 | 36 | (defn alert 37 | ([title] 38 | (.alert Alert title)) 39 | ([title msg] 40 | (.alert Alert title msg)) 41 | ([title msg buttons] 42 | (.alert Alert title msg buttons)) 43 | ([title msg buttons options] 44 | (.alert Alert title msg buttons options)) 45 | ([title msg buttons options type] 46 | (.alert Alert title msg buttons options type))) 47 | 48 | 49 | ;; Components 50 | (def text (adapt-fn (.-Text ReactNative))) 51 | (def view (adapt-fn (.-View ReactNative))) 52 | (def image (adapt-fn (.-Image ReactNative))) 53 | (def image-background (adapt-fn (.-ImageBackground ReactNative))) 54 | (def touchable-highlight (adapt-fn (.-TouchableHighlight ReactNative))) 55 | (def touchable-native-feedback (adapt-fn (.-TouchableNativeFeedback ReactNative))) 56 | (def touchable-without-feedback (adapt-fn (.-TouchableWithoutFeedback ReactNative))) 57 | (def touchable-opacity (adapt-fn (.-TouchableOpacity ReactNative))) 58 | (def activity-indicator (adapt-fn (.-ActivityIndicator ReactNative))) 59 | (def button (adapt-fn (.-Button ReactNative))) 60 | (def date-picker-ios (adapt-fn (.-DatePickerIOS ReactNative))) 61 | (def drawer-layout-android (adapt-fn (.-DrawerLayoutAndroid ReactNative))) 62 | (def flat-list (adapt-fn (.-FlatList ReactNative))) 63 | (def input-accessory-view (adapt-fn (.-InputAccessoryView ReactNative))) 64 | (def keyboard-avoiding-view (adapt-fn (.-KeyboardAvoidingView ReactNative))) 65 | (def modal (adapt-fn (.-Modal ReactNative))) 66 | (def picker (adapt-fn (.-Picker ReactNative))) 67 | (def picker-item (adapt-fn (.. ReactNative -Picker -Item))) 68 | (def picker-ios (adapt-fn (.-PickerIOS ReactNative))) 69 | (def progress-bar-android (adapt-fn (.-ProgressBarAndroid ReactNative))) 70 | (def progress-view-ios (adapt-fn (.-ProgressViewIOS ReactNative))) 71 | (def refresh-control (adapt-fn (.-RefreshControl ReactNative))) 72 | (def safe-area-view (adapt-fn (.-SafeAreaView ReactNative))) 73 | (def scroll-view (adapt-fn (.-ScrollView ReactNative))) 74 | (def section-list (adapt-fn (.-SectionList ReactNative))) 75 | (def segmented-control-ios (adapt-fn (.-SegmentedControlIOS ReactNative))) 76 | (def snapshot-view-ios (adapt-fn (.-SnapshotViewIOS ReactNative))) 77 | (def status-bar (adapt-fn (.-StatusBar ReactNative))) 78 | (def switch (adapt-fn (.-Switch ReactNative))) 79 | (def text-input (adapt-fn (.-TextInput ReactNative))) 80 | (def toolbar-android (adapt-fn (.-ToolbarAndroid ReactNative))) 81 | (def virtualized-list (adapt-fn (.-VirtualizedList ReactNative))) 82 | 83 | 84 | ;; StyleSheet 85 | (def StyleSheet (.-StyleSheet ReactNative)) 86 | 87 | (defn convert-prop-value [x] 88 | (cond 89 | (compiler/js-val? x) x 90 | (compiler/named? x) (name x) 91 | (map? x) (reduce-kv compiler/kv-conv #js {} x) 92 | :else (clj->js x))) 93 | 94 | (defn create-stylesheet [styles] 95 | (let [compiled (->> styles 96 | (reduce-kv 97 | (fn [o k v] 98 | (gobj/set o (name k) (convert-prop-value v)) 99 | o) 100 | #js {}) 101 | (.create StyleSheet))] 102 | (js->clj compiled :keywordize-keys true))) 103 | -------------------------------------------------------------------------------- /core/benchmark/uix/react.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.react) 2 | 3 | (defn InputField 4 | [^js props] 5 | (.createElement 6 | js/React 7 | "input" 8 | #js {:className #js ["form-control" (aget #js {:large "form-control-lg"} (.-size props))], 9 | :type (.-type props), 10 | :placeholder (.-placeholder props), 11 | :style #js {:border "1px solid blue", :borderRadius 3, :padding "4px 8px"}})) 12 | 13 | (defn Button 14 | [^js props] 15 | (.createElement 16 | js/React 17 | "button" 18 | #js 19 | {:className 20 | #js 21 | ["btn" (aget #js {:large "btn-large"} (.-size props)) 22 | (aget #js {:primary "btn-primary"} (.-kind props)) 23 | (.-className props)], 24 | :style 25 | #js 26 | {:padding "8px 24px", 27 | :color "white", 28 | :background "blue", 29 | :fontSize "11px", 30 | :textTransform "uppercase", 31 | :textAlign "center"}} 32 | (.-children props))) 33 | 34 | (defn Fieldset 35 | [^js props] 36 | (.createElement 37 | js/React 38 | "fieldset" 39 | #js {:className "form-group", :style #js {:padding 8, :border "none"}} 40 | (.-children props))) 41 | 42 | (defn Form [props] (.createElement js/React "form" #js {} (.-children props))) 43 | 44 | (defn Row 45 | [^js props] 46 | (.createElement js/React "div" #js {:className "row"} (.-children props))) 47 | 48 | (defn Col 49 | [^js props] 50 | (.createElement 51 | js/React 52 | "div" 53 | #js 54 | {:className 55 | #js 56 | [(str "col-md-" (.-md props)) (str "col-xs-" (.-xs props)) 57 | (str "offset-md-" (.-offsetMd props))]} 58 | (.-children props))) 59 | 60 | (defn Editor 61 | [] 62 | (.createElement 63 | js/React 64 | "div" 65 | #js {:className "editor-page"} 66 | (.createElement 67 | js/React 68 | "div" 69 | #js {:className "container page"} 70 | (.createElement 71 | js/React 72 | Row 73 | #js {} 74 | (.createElement 75 | js/React 76 | Col 77 | #js {:md 10, :xs 12, :offsetMd 1} 78 | (.createElement 79 | js/React 80 | Form 81 | #js {} 82 | (.createElement 83 | js/React 84 | "fieldset" 85 | #js {} 86 | (.createElement 87 | js/React 88 | Fieldset 89 | #js {} 90 | (.createElement 91 | js/React 92 | InputField 93 | #js 94 | {:type "text", :placeholder "Article Title", :size "large"})) 95 | (.createElement 96 | js/React 97 | Fieldset 98 | #js {} 99 | (.createElement 100 | js/React 101 | InputField 102 | #js 103 | {:type "text", :placeholder "What's this article about?"})) 104 | (.createElement 105 | js/React 106 | Fieldset 107 | #js {} 108 | (.createElement 109 | js/React 110 | InputField 111 | #js 112 | {:rows 8, 113 | :fieldType "textarea", 114 | :placeholder "Write your article (in markdown)"})) 115 | (.createElement 116 | js/React 117 | Fieldset 118 | #js {} 119 | (.createElement 120 | js/React 121 | InputField 122 | #js {:type "text", :placeholder "Enter tags"}) 123 | (.createElement js/React "div" #js {:className "tag-list"})) 124 | (.createElement 125 | js/React 126 | Button 127 | #js {:size "large", :kind "primary", :className "pull-xs-right"} 128 | "Update Article")))))))) 129 | 130 | -------------------------------------------------------------------------------- /core/dev/uix/recipes/global_state.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.recipes.global-state 2 | "This recipe shows how UIx apps can architect global data store 3 | and effects handling using Hooks API." 4 | (:require [uix.core.alpha :as uix :refer [defui]] 5 | [xframe.core.alpha :as xf :refer [ (js/fetch url) 73 | (.then #(if (.-ok %) 74 | (.json %) 75 | (xf/dispatch [on-failed %]))) 76 | (.then bean/->clj) 77 | (.then #(xf/dispatch [on-ok %])))))) 78 | 79 | ;; UI components 80 | (defn repo-item [idx] 81 | (let [{:keys [name description]} ( 120 | [form] 121 | (when loading? 122 | [:div "Loading repos..."]) 123 | (when error 124 | [:div {:style {:color "red"}} 125 | (.-message error)]) 126 | [repos-list]])) 127 | 128 | 129 | ;; Init database 130 | (defonce init-db 131 | (xf/dispatch [:db/init])) 132 | -------------------------------------------------------------------------------- /core/test/uix/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.core-test 2 | (:require [clojure.test :refer [deftest is async testing run-tests]] 3 | [uix.core.alpha :as uix.core :refer [html defui defcontext]] 4 | ;[uix.core.lazy-loader :refer [require-lazy]] 5 | [uix.lib] 6 | [react :as r] 7 | [uix.test-utils :as t] 8 | [cljs-bean.core :as bean] 9 | [clojure.string :as str])) 10 | 11 | (deftest test-lib 12 | (is (= (seq (uix.lib/re-seq* (re-pattern "foo") "foo bar foo baz foo zot")) 13 | (list "foo" "foo" "foo"))) 14 | 15 | (is (= (map vec (uix.lib/re-seq* (re-pattern "f(.)o") "foo bar foo baz foo zot")) 16 | (list ["foo" "o"] ["foo" "o"] ["foo" "o"]))) 17 | 18 | (is (= '("") (seq (uix.lib/re-seq* #"\s*" ""))))) 19 | 20 | (deftest test-strict-mode 21 | (is (= (uix.core/strict-mode 1) [:> r/StrictMode 1]))) 22 | 23 | (deftest test-create-ref 24 | (let [ref (uix.core/create-ref 1)] 25 | (is (= (type ref) uix.core/ReactRef)) 26 | (is (= @ref 1)))) 27 | 28 | (deftest test-memoize 29 | (let [f (uix.core/memoize (fn [x] 30 | (is (= 1 x)) 31 | [:h1 x]))] 32 | (is (t/react-element-of-type? f "react.memo")) 33 | (is (= "

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/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/romanliutikov) 20 | 21 | [![CircleCI](https://circleci.com/gh/roman01la/uix.svg?style=svg)](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 | [![Clojars Project](https://img.shields.io/clojars/v/uix/core.svg)](https://clojars.org/uix/core) 26 | [![Clojars Project](https://img.shields.io/clojars/v/uix/dom.svg)](https://clojars.org/uix/dom) 27 | [![Clojars Project](https://img.shields.io/clojars/v/uix/rn.svg)](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 []) 27 | rf (fn []) 28 | rf-memo (fn [])] 29 | (uixc/with-name f rf rf-memo) 30 | (is (= (.-displayName rf) "uix.compiler-test/")) 31 | (is (= (.-displayName rf-memo) "memo(uix.compiler-test/)"))) 32 | (let [f (fn []) 33 | rf (fn []) 34 | rf-memo (fn [])] 35 | (set! (.-displayName f) "[custom name!]") 36 | (uixc/with-name f rf rf-memo) 37 | (is (= (.-displayName rf) "[custom name!]")) 38 | (is (= (.-displayName rf-memo) "memo([custom name!])"))) 39 | (let [f (js-obj) 40 | rf (fn []) 41 | rf-memo (fn [])] 42 | (uixc/with-name f rf rf-memo) 43 | (is (not (exists? (.-displayName rf)))) 44 | (is (not (exists? (.-displayName rf-memo))))) 45 | 46 | (set! uixc/*format-display-name* (fn [s] (str "[" s "=>" (uixc/default-format-display-name s) "]"))) 47 | (let [f (fn []) 48 | rf (fn []) 49 | rf-memo (fn [])] 50 | (uixc/with-name f rf rf-memo) 51 | (is (= (.-displayName rf) "[uix$compiler_test$_LT_some_component_GT_=>uix.compiler-test/]")) 52 | (is (= (.-displayName rf-memo) "memo([uix$compiler_test$_LT_some_component_GT_=>uix.compiler-test/])"))) 53 | (let [f (fn []) 54 | rf (fn []) 55 | rf-memo (fn [])] 56 | (set! (.-displayName f) "[custom name!]") 57 | (uixc/with-name f rf rf-memo) 58 | (is (= (.-displayName rf) "[[custom name!]=>[custom name!]]")) 59 | (is (= (.-displayName rf-memo) "memo([[custom name!]=>[custom name!]])"))) 60 | 61 | ; nil returned from *format-display-name* means no name 62 | (set! uixc/*format-display-name* (fn [_s _orig])) 63 | (let [f (fn []) 64 | rf (fn []) 65 | rf-memo (fn [])] 66 | (uixc/with-name f rf rf-memo) 67 | (is (not (exists? (.-displayName rf)))) 68 | (is (not (exists? (.-displayName rf-memo))))) 69 | (let [f (fn []) 70 | rf (fn []) 71 | rf-memo (fn [])] 72 | (set! (.-displayName f) "[custom name!]") 73 | (uixc/with-name f rf rf-memo) 74 | (is (not (exists? (.-displayName rf)))) 75 | (is (not (exists? (.-displayName rf-memo))))) 76 | 77 | (let [f (fn []) 78 | rf (fn []) 79 | rf-memo (fn [])] 80 | (set! uixc/*format-display-name* nil) 81 | (is (thrown-with-msg? js/Error #"\*format-display-name\* is not bound" 82 | (uixc/with-name f rf rf-memo))))))) 83 | 84 | (deftest test-parse-tag 85 | (is (= (js->clj (uixc/parse-tag (name :div#id.class))) 86 | ["div" "id" ["class"] false])) 87 | (is (= (js->clj (uixc/parse-tag (name :#id.class))) 88 | ["div" "id" ["class"] false])) 89 | (is (= (js->clj (uixc/parse-tag (name :.class1.class2#id))) 90 | ["div" "id" ["class1" "class2"] false])) 91 | (is (= (js->clj (uixc/parse-tag (name :.#id1#id2))) 92 | ["div" "id2" [] false])) 93 | (is (= (js->clj (uixc/parse-tag (name :custom-tag))) 94 | ["custom-tag" nil [] true]))) 95 | 96 | (deftest test-class-names 97 | (is (= (uixc/class-names) nil)) 98 | (testing "Named types" 99 | (is (= (uixc/class-names "a") "a")) 100 | (is (= (uixc/class-names :a) "a"))) 101 | (testing "Collection of classes" 102 | (is (= (uixc/class-names [1 2 3]) "1 2 3")) 103 | (is (= (uixc/class-names [1 :a "b"]) "1 a b"))) 104 | (testing "Map of class -> boolean" 105 | (is (= (uixc/class-names {:c1 true :c2 false} "c1"))))) 106 | 107 | (deftest test-set-id-class 108 | (testing "Hiccup classes should preceding attribute classes" 109 | (is (= (uixc/set-id-class {:class "a"} (uixc/parse-tag (name :.b))) 110 | {:class "b a"}))) 111 | (testing "Attribute ID has higher priority than Hiccup ID" 112 | (is (= (uixc/set-id-class {:id "a"} (uixc/parse-tag (name :#b))) 113 | {:id "a"})))) 114 | 115 | (deftest test-add-transform-fn 116 | (uixc/add-transform-fn identity) 117 | (is (= @uixc/transform-fns #{identity}))) 118 | 119 | (deftest test-cached-prop-name 120 | (are [dash camel] (= (uixc/cached-prop-name dash) camel) 121 | ; keywords 122 | :key "key" 123 | :name1-name2-name3 "name1Name2Name3" 124 | :data-name "data-name" 125 | :aria-name "aria-name" 126 | ; symbols 127 | 'key "key" 128 | 'name1-name2-name3 "name1Name2Name3" 129 | 'data-name "data-name" 130 | 'aria-name "aria-name" 131 | ; strings 132 | "key" "key" 133 | "name1-name2-name3" "name1-name2-name3" 134 | "data-name" "data-name" 135 | "aria-name" "aria-name")) 136 | 137 | (deftest cached-prop-name 138 | (is (= "className" (uixc/cached-prop-name :class)))) 139 | 140 | (deftest cached-custom-prop-name 141 | (is (= "helloWorld" (uixc/cached-prop-name :hello-world)))) 142 | 143 | (deftest convert-props-test 144 | (is (js-equal? #js {:className "a"} 145 | (uixc/convert-props {:class "a"} [] true))) 146 | (is (js-equal? #js {:className "a b" :id "a"} 147 | (uixc/convert-props {:class "b"} #js [nil "a" #js ["a"]] true)))) 148 | 149 | 150 | (deftest to-string-test [] 151 | (let [comp (fn [props] 152 | [:div (str "i am " (:foo props))])] 153 | (is (re-find #"i am foobar" 154 | (as-string [comp {:foo "foobar"}]))))) 155 | 156 | (deftest data-aria-test [] 157 | (is (re-find #"data-foo" 158 | (as-string [:div {:data-foo "x"}]))) 159 | (is (re-find #"aria-labelledby" 160 | (as-string [:div {:aria-labelledby "x"}]))) 161 | (is (re-find #"enc[tT]ype" 162 | (as-string [:div {"encType" "x"}])) 163 | "Strings are passed through to React, and have to be camelcase.") 164 | (is (re-find #"enc[tT]ype" 165 | (as-string [:div {:enc-type "x"}])) 166 | "Strings are passed through to React, and have to be camelcase.")) 167 | 168 | (deftest dynamic-id-class [] 169 | (is (re-find #"id=.foo" 170 | (as-string [:div#foo {:class "bar"}]))) 171 | (is (re-find #"class=.foo bar" 172 | (as-string [:div.foo {:class "bar"}]))) 173 | (is (re-find #"class=.foo bar" 174 | (as-string [:div.foo.bar]))) 175 | (is (re-find #"id=.foo" 176 | (as-string [:div#foo.foo.bar]))) 177 | (is (re-find #"class=.xxx bar" 178 | (as-string [:div#foo.xxx.bar]))) 179 | (is (re-find #"id=.foo" 180 | (as-string [:div.bar {:id "foo"}]))) 181 | (is (re-find #"id=.foo" 182 | (as-string [:div.bar.xxx {:id "foo"}]))) 183 | (is (re-find #"id=.foo" 184 | (as-string [:div#bar {:id "foo"}])) 185 | "Dynamic id overwrites static")) 186 | 187 | (deftest ifn-component [] 188 | (defmulti my-div :type) 189 | (defmethod my-div :fooish [child] [:div.foo (:content child)]) 190 | (defmethod my-div :barish [child] [:div.bar (:content child)]) 191 | 192 | (let [comp {:foo [:div "foodiv"] 193 | :bar [:div "bardiv"]}] 194 | (is (re-find #"foodiv" 195 | (as-string [:div [comp :foo]]))) 196 | (is (re-find #"bardiv" 197 | (as-string [:div [comp :bar]]))) 198 | (is (re-find #"class=.foo" 199 | (as-string [my-div {:type :fooish :content "inner"}]))))) 200 | 201 | (deftest test-null-component 202 | (let [null-comp (fn [do-show] 203 | (when do-show 204 | [:div "div in test-null-component"]))] 205 | (is (not (re-find #"test-null-component" 206 | (as-string [null-comp false])))) 207 | (is (re-find #"test-null-component" 208 | (as-string [null-comp true]))))) 209 | 210 | (deftest test-class-from-collection 211 | (is (= (as-string [:p {:class ["a" "b" "c" "d"]}]) 212 | (as-string [:p {:class "a b c d"}]))) 213 | (is (= (as-string [:p {:class ["a" nil "b" false "c" nil]}]) 214 | (as-string [:p {:class "a b c"}]))) 215 | (is (= (as-string [:p {:class '("a" "b" "c")}]) 216 | (as-string [:p {:class "a b c"}]))) 217 | (is (= (as-string [:p {:class #{"a" "b" "c"}}]) 218 | (as-string [:p {:class "a b c"}])))) 219 | 220 | (deftest test-keys 221 | (let [c (fn key-tester [] 222 | [:div 223 | (for [i (range 3)] 224 | ^{:key i} [:p i]) 225 | (for [i (range 3)] 226 | [:p {:key i} i])])] 227 | (with-error #(as-string [c])))) 228 | 229 | (deftest class-different-types 230 | (testing "named values are supported" 231 | (is (= (as-string [:p {:class :a}]) 232 | (as-string [:p {:class "a"}]))) 233 | (is (= (as-string [:p.a {:class :b}]) 234 | (as-string [:p {:class "a b"}]))) 235 | (is (= (as-string [:p.a {:class 'b}]) 236 | (as-string [:p {:class "a b"}]))) 237 | (is (= (as-string [:p {:class [:a :b]}]) 238 | (as-string [:p {:class "a b"}]))) 239 | (is (= (as-string [:p {:class ['a :b]}]) 240 | (as-string [:p {:class "a b"}])))) 241 | 242 | (testing "non-named values like numbers" 243 | (is (= (as-string [:p {:class [1 :b]}]) 244 | (as-string [:p {:class "1 b"}])))) 245 | 246 | (testing "falsey values are filtered from collections" 247 | (is (= (as-string [:p {:class [:a :b false nil]}]) 248 | (as-string [:p {:class "a b"}]))))) 249 | 250 | 251 | (deftest style-property-names-are-camel-cased 252 | (is (re-find #"
foo
" 253 | (as-string [:div {:style {:text-align "center"}} "foo"])))) 254 | 255 | (deftest custom-element-class-prop 256 | (is (re-find #"foo" 257 | (as-string [:custom-element {:class "foobar"} "foo"]))) 258 | 259 | (is (re-find #"foo" 260 | (as-string [:custom-element.foobar "foo"])))) 261 | 262 | (deftest test-fragments 263 | (testing "Fragment as array" 264 | (let [comp (fn comp1 [] 265 | #js [(uixc/as-element [:div "hello"]) 266 | (uixc/as-element [:div "world"])])] 267 | (is (= "
hello
world
" 268 | (as-string [comp]))))) 269 | 270 | (testing "Fragment element, :<>" 271 | (let [comp (fn comp2 [] 272 | [:<> 273 | [:div "hello"] 274 | [:div "world"] 275 | [:div "foo"]])] 276 | (is (= "
hello
world
foo
" 277 | (as-string [comp]))))) 278 | 279 | (testing "Fragment key" 280 | ;; This would cause React warning if both fragments didn't have key set 281 | ;; But wont fail the test 282 | (let [children (fn comp4 [] 283 | [:<> 284 | [:div "foo"]]) 285 | comp (fn comp3 [] 286 | [:div 287 | (list 288 | [:<> 289 | {:key 1} 290 | [:div "hello"] 291 | [:div "world"]] 292 | ^{:key 2} 293 | [children] 294 | ^{:key 3} 295 | [:<> 296 | [:div "1"] 297 | [:div "2"]])])] 298 | (is (= "
hello
world
foo
1
2
" 299 | (as-string [comp])))))) 300 | 301 | (deftest test-suspense 302 | (is (.-type (uixc/as-element [:# {:fallback 1} 2])) 303 | (symbol-for "react.suspense"))) 304 | 305 | (deftest test-interop 306 | (testing "Interop element type" 307 | (is (.-type (uixc/as-element [:> inc])) 308 | inc)) 309 | (testing "Shallowly converted props" 310 | (let [el (uixc/as-element [:> inc {:a 1 :b {:c 2}} :child]) 311 | props (.-props el)] 312 | (is (.-a props) 1) 313 | (is (.-b props) {:c 2}) 314 | (is (.-children props) :child)))) 315 | 316 | (deftest test-portal 317 | (try 318 | (uixc/as-element [:-> 1 2]) 319 | (catch :default e 320 | (is "Target container is not a DOM element." (.-message e))))) 321 | 322 | (deftest test-as-react 323 | (let [h1 (uixc/as-react #(do 324 | (is (map? %) true) 325 | (is "TEXT" (:text %)) 326 | [:h1 (:text %)])) 327 | el (h1 #js {:text "TEXT"}) 328 | props (.-props el)] 329 | (is (.-type el) "h1") 330 | (is (.-children props) "TEXT"))) 331 | 332 | (defn -main [] 333 | (run-tests)) 334 | -------------------------------------------------------------------------------- /core/test/uix/ssr_test.cljc: -------------------------------------------------------------------------------- 1 | (ns uix.ssr-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [uix.compiler.alpha :as uixc] 4 | [uix.dom.alpha :as dom] 5 | [clojure.string :as str] 6 | #?@(:cljs [[fs :as fs] 7 | ["react-dom/server" :as dom-server]]) 8 | #?@(:clj [[clojure.java.shell :as shell] 9 | [clojure.java.io :as io] 10 | [clj-diffmatchpatch :as diff]]))) 11 | 12 | #?(:cljs 13 | (set! (.-ReactDOMServer js/global) dom-server)) 14 | 15 | #?(:clj 16 | (defn render [el] 17 | (uixc/render-to-static-markup el))) 18 | 19 | #?(:clj 20 | (deftest test-class-names 21 | (is (= (render [:div]) "
")) 22 | (testing "Named types" 23 | (is (= (render [:div {:class "a"}]) "
")) 24 | (is (= (render [:div {:class :a}]) "
"))) 25 | (testing "Collection of classes" 26 | (is (= (render [:div {:class [1 2 3]}]) "
")) 27 | (is (= (render [:div {:class [1 :a "b"]}]) "
"))) 28 | (testing "Map of class -> boolean" 29 | (is (= (render [:div {:class {:c1 true :c2 false}}]) "
"))))) 30 | 31 | #?(:clj 32 | (deftest test-46 33 | (is (= (render [:div#id "hi"]) "
hi
")) 34 | (is (= (render [:div#id {:id 1} "hi"]) "
hi
")) 35 | (is (= (render [:div {:id 1} "hi"]) "
hi
")) 36 | 37 | (is (= (render [:div.a.b "hi"]) "
hi
")) 38 | (is (= (render [:div.a.b {:class "c"} "hi"]) "
hi
")) 39 | (is (= (render [:div {:class "c"} "hi"]) "
hi
")) 40 | (is (= (render [:div {:class ["c"]} "hi"]) "
hi
")) 41 | (is (= (render [:div.a.b {:class ["c"]} "hi"]) "
hi
")))) 42 | 43 | ;; Adapted from https://github.com/tonsky/rum/blob/gh-pages/test/rum/test/server_render.cljc 44 | 45 | (defn comp-simple [] 46 | [:div 47 | [:div "A" 48 | [:span "A1"] 49 | [:span "A2"]] 50 | [:div "B"] 51 | [:div "C" "D"] 52 | [:div "E" 53 | [:span "E1"]] 54 | [:div nil] 55 | [:div nil "F"] 56 | [:div {} ((constantly nil)) "G"]]) 57 | 58 | (defn comp-tag [] 59 | [:div.header#up "test"]) 60 | 61 | 62 | (defn comp-list [] 63 | [:ul [:li {:key "F"}] [:li {:key "M"}]]) 64 | 65 | 66 | (defn comp-lists [] 67 | [:div 68 | [:.a (list ^{:key "b"} [:.b])] 69 | [:.c "d" (list ^{:key "e"} [:.e])] 70 | [:.f "g" (list ^{:key "i"} [:.h]) "i"]]) 71 | 72 | 73 | (defn comp-root-array [] 74 | (list 75 | [:.a "A"] 76 | [:.b "B"] 77 | [:.c "C"])) 78 | 79 | (defn comp-header [] 80 | [:ul.nav__content 81 | (list [:li.menu-item {:key "F"} "Женщинам"] 82 | [:li.menu-item {:key "M"} "Мужчинам"]) 83 | [:li.menu-item {:key "outlet"} "Outlet"]]) 84 | 85 | 86 | (defn comp-nil1 [] 87 | "In this case nil will be not counted against reactid" 88 | [:div {:class "parent"} 89 | nil 90 | [:div.child]]) 91 | 92 | 93 | (defn comp-nil2 [] 94 | "In this case *both* nils will be counted against reactid" 95 | [:div {:class "parent"} 96 | nil 97 | [:div.child] 98 | (list 99 | nil 100 | [:div.child2])]) 101 | 102 | (defn comp-nothing [] 103 | nil) 104 | 105 | (defn comp-nothing2 [] 106 | [:div 107 | [:div [comp-nothing]] 108 | [:div "a" [comp-nothing]] 109 | [:div [comp-nothing] "b"] 110 | [:div "a" [comp-nothing] "b" [:span "x"]] 111 | [:div [:.a] [comp-nothing] [:.b]] 112 | [:div ^{:key "K"} [comp-nothing]]]) 113 | 114 | (defn comp-span [] 115 | [:span 116 | "a" "b" 117 | "a" [:tag "b"] "c" 118 | "a" (list "b" "c") "d" 119 | "a" (list "b" "c") (list "d" "e") "f" 120 | (list "a" "b") [:tag "c"] (list "d" "e") 121 | "a" nil "b" 122 | "a" [comp-nothing] "b" 123 | "a" (list nil) "b"]) 124 | 125 | (defn comp-campaign [] 126 | [:div#today.content.wrapper 127 | (list 128 | [:div.banner {:class " big " 129 | :style {:background-image "url(123)"} 130 | :key "campaign-20871"}] 131 | [:a.banner__item-link {:href "/catalogue/s-10079-colin-s/"}] 132 | 133 | [:div.banner {:class " " 134 | :key "banner-:promo"}] 135 | [:a.banner__item-link {:href nil, :target "_blank"}] 136 | 137 | [:div.banner {:class " medium " 138 | :style {:background-image "url(321)"} 139 | :key "campaign-20872"}] 140 | [:a.banner__item-link {:href "/catalogue/s-10089-rinascimento/"}])]) 141 | 142 | (defn comp-styles [] 143 | [:div 144 | [:div.a {:style {}}] 145 | [:div.b {:style {:background-color nil}}] 146 | [:div.c {:style {:background-color ""}}] 147 | [:div.d {:style 148 | {:background-image "url(\"123\")" ;; should escape quotes 149 | :line-height 24 ;; unitless, should not add 'px' 150 | :-webkit-box-flex 3 ;; prefixed unitless 151 | :margin-top 17 ;; should add 'px' 152 | :margin-right 17.1 153 | :margin-left 0}}] ;; no 'px' added to 0 154 | [:div.e {:style 155 | {:border-width " 1 " ;; trim numeric & append 'px' 156 | :padding-right " 1.2 " 157 | :padding-bottom "1em" ;; do not add 'px' if unit already specified 158 | :text-align " left " ;; trim non-numeric values 159 | :flex-grow " 1 "}}] ;; trim unitless values 160 | [:div.f {:style 161 | {:background-image "url('123')" ;; should escape quotes 162 | :fontWeight 10 ;; should convert from react-style properties to CSS 163 | "WebkitFlex" 1 ;; prefixed react-style prop 164 | "msFlex" 1 ;; prefixed react-style prop (lowecase ms) 165 | "zIndex" 1}}]]) ;; accept strings too 166 | 167 | (defn comp-attrs [] 168 | [:div 169 | (for [[a v] [[:data-attr-ibute "b"] ;; should not touch data-* and aria* attr names 170 | [:aria-checked "c"] 171 | [:form-enc-type "text/plain"] ;; should normalize (remove dashes) 172 | [:checked false] ;; nil and false attrs not printed 173 | [:allow-full-screen true] ;; true printed as attr="" 174 | [:href "/a=b&c=d"]]] 175 | ^{:key v} 176 | [:div {a v}])]) 177 | 178 | (defn comp-attrs-capitalization [] 179 | [:div 180 | (for [a [:accept-charset :access-key :allow-transparency :auto-complete :cell-padding :cell-spacing :char-set :class-id :content-editable :context-menu :cross-origin :date-time :enc-type :form-action :form-enc-type :form-method :form-target :frame-border :href-lang :http-equiv :input-mode :key-params :key-type :margin-height :margin-width :max-length :media-group :min-length :radio-group :referrer-policy :spell-check :src-doc :src-lang :src-set :tab-index :use-map :auto-capitalize :auto-correct :auto-save :item-prop :item-type :item-id :item-ref]] 181 | ^{:key a} [:div {a "_"}]) 182 | 183 | [:table 184 | [:td {:col-span 1 185 | :row-span 1}]] 186 | 187 | [:svg 188 | (for [a [:allow-reorder :attribute-name :attribute-type :auto-reverse :base-frequency :base-profile :calc-mode :clip-path-units :content-script-type :content-style-type :diffuse-constant :edge-mode :external-resources-required :filter-res :filter-units :glyph-ref :gradient-transform :gradient-units :kernel-matrix :kernel-unit-length :key-points :key-splines :key-times :length-adjust :limiting-cone-angle :marker-height :marker-units :marker-width :mask-content-units :mask-units :num-octaves :path-length :pattern-content-units :pattern-transform :pattern-units :points-at-x :points-at-y :points-at-z :preserve-alpha :preserve-aspect-ratio :primitive-units :ref-x :ref-y :repeat-count :repeat-dur :required-extensions :required-features :specular-constant :specular-exponent :spread-method :start-offset :std-deviation :stitch-tiles :surface-scale :system-language :table-values :target-x :target-y :view-box :view-target :x-channel-selector :xlink-actuate :xlink-arcrole :xlink-href :xlink-role :xlink-show :xlink-title :xlink-type :xml-base :xmlns-xlink :xml-lang :xml-space :y-channel-selector :zoom-and-pan]] 189 | [:path {a "_"}])] 190 | 191 | (for [a [:allow-full-screen :auto-play :form-no-validate :no-validate :read-only :item-scope]] 192 | ^{:key a} [:div {a true}])]) 193 | 194 | (defn comp-attrs-order [] 195 | [:div 196 | [:a {:title "a" 197 | :alt "b" 198 | :rel "c" 199 | :target "d" 200 | :src "e"}] 201 | [:a {:src "a" 202 | :target "b" 203 | :rel "c" 204 | :alt "d" 205 | :title "e"}] 206 | [:a {:title "a" :class "b" :rel "d"}] 207 | [:a {:title "a" :class ["b" "c"] :rel "d"}] 208 | [:a.clazz {:title "a" :class "b" :rel "d"}] 209 | [:a.clazz {:title "a" :class ["b" "c"] :rel "d"}] 210 | [:a.clazz#id {:title "a"}] 211 | [:a#id.clazz {:title "a"}] 212 | [:a.clazz#id {:title "a" :class "b"}] 213 | [:a#clazz.id {:title "a" :class "b"}]]) 214 | 215 | (defn comp-classes [] 216 | [:div 217 | [:div {:class [nil]}] 218 | [:div {:class :c3}] 219 | [:div {:class [:c3 :c4]}] ;; list form 220 | [:div {:class "c3"}] ;; string form 221 | [:div {:class ["c3" "c4"]}] 222 | [:div {:class [" c3 " " c4 "]}] ;; trimming 223 | [:div {:class [:c3 nil :c4]}] ;; nils are not removed 224 | [:div {:class [:c2 :c3]}] ;; removing duplicates 225 | [:.c1 {:class nil}] 226 | [:.c1 {:class (when false "...")}] ;; see #99 227 | [:.c1.c2 {:class :c3}] 228 | [:.c1.c2 {:class [:c3 :c4]}] ;; list form 229 | [:.c1.c2 {:class "c3"}] ;; string form 230 | [:.c1.c2 {:class ["c3" "c4"]}] 231 | [:.c1.c2 {:class [" c3 " " c4 "]}] ;; trimming 232 | [:.c1.c2 {:class [:c3 nil :c4]}] ;; nils are not removed 233 | [:.c1.c2 {:class [:c2 :c3]}]]) ;; not removing duplicates 234 | 235 | 236 | (defn comp-html [] 237 | [:div {:dangerouslySetInnerHTML {:__html "test"}}]) 238 | 239 | (defn comp-inputs [] 240 | [:div 241 | [:input#id {:class "x" :type "text" :auto-complete "off"}] 242 | [:input {:type "text" :default-value "x"}] 243 | [:input {:type "checkbox" :default-checked true}] 244 | [:input {:type "radio" :default-checked true}] 245 | [:select {:default-value "A"} 246 | [:option {:value "A"} "Apple"] 247 | [:option {:value "B"} "Banana"]] 248 | [:select {:value "A"} 249 | [:option#id.class {:value "A"} "Apple"] 250 | [:option#id.class {:value "B"} "Banana"]] 251 | [:textarea {:value "text"}] 252 | [:textarea "text"] 253 | [:textarea {:default-value "text"}]]) 254 | 255 | (defn comp-svg [] 256 | [:svg.cclogo 257 | {:width 100 258 | :height 100 259 | :view-box "0 232.5 333.2 232.5" ;; should be rendered as viewBox 260 | :vector-effect "effect" ;; should be rendered as vector-effect 261 | :version "1.1" 262 | :dangerouslySetInnerHTML {:__html "[...tons of raw SVG removed...]"}}]) 263 | 264 | 265 | (defn comp-aria [] 266 | [:div 267 | {:aria-hidden true 268 | :aria-readonly false 269 | :aria-disabled "true" 270 | :aria-checked "false"}]) 271 | 272 | (def components 273 | {"simple" comp-simple 274 | "tag" comp-tag 275 | "list" comp-list 276 | "lists" comp-lists 277 | "root-array" comp-root-array 278 | "header" comp-header 279 | "nil1" comp-nil1 280 | "nil2" comp-nil2 281 | "nothing" comp-nothing 282 | "nothing2" comp-nothing2 283 | "span" comp-span 284 | "campaign" comp-campaign 285 | "styles" comp-styles 286 | "attrs" comp-attrs 287 | "attrs-cap" comp-attrs-capitalization 288 | "attrs-order" comp-attrs-order 289 | "classes" comp-classes 290 | "html" comp-html 291 | "inputs" comp-inputs 292 | "svg" comp-svg 293 | "aria" comp-aria}) 294 | 295 | (def render-dir "server_render_test") 296 | 297 | #?(:cljs 298 | (defn -main [& args] 299 | (doseq [[name f] components] 300 | (let [html (dom/render-to-string [f]) 301 | path (str render-dir "/html/" name ".html")] 302 | (fs/writeFileSync path html)) 303 | (let [html (dom/render-to-static-markup [f]) 304 | path (str render-dir "/markup/" name ".html")] 305 | (fs/writeFileSync path html))))) 306 | 307 | #?(:clj 308 | (defn exec [& cmd] 309 | (testing cmd 310 | (println "Running" (str "\"" (str/join " " cmd) "\"")) 311 | (let [{:keys [exit out err]} (apply shell/sh cmd)] 312 | (is (= exit 0)) 313 | (when-not (str/blank? err) 314 | (binding [*out* *err*] 315 | (println err))) 316 | (when-not (str/blank? out) 317 | (println out)))))) 318 | 319 | #?(:clj 320 | (defn diff [s1 s2] 321 | (->> (diff/wdiff s1 s2) 322 | (map (fn [[op text]] 323 | (case op 324 | :delete (str "\033[37;41;1m" text "\033[0m") 325 | :insert (str "\033[37;42;1m" text "\033[0m") 326 | :equal text))) 327 | (str/join)))) 328 | 329 | #?(:clj 330 | (deftest test-ssr-compat-between-jvm-and-js 331 | (doseq [^java.io.File f (reverse (file-seq (io/file render-dir)))] 332 | (when (.exists f) 333 | (.delete f))) 334 | (.mkdir (io/file render-dir)) 335 | (.mkdir (io/file render-dir "html")) 336 | (.mkdir (io/file render-dir "markup")) 337 | (exec "clojure" "-A:test" "-m" "cljs.main" "-m" "uix.ssr-test" "-re" "node") 338 | (doseq [[name f] components] 339 | (testing name 340 | (let [react-html (slurp (str render-dir "/html/" name ".html")) 341 | uix-html (uixc/render-to-string [f])] 342 | (is (= react-html uix-html) (diff react-html uix-html))) 343 | (let [react-html (slurp (str render-dir "/markup/" name ".html")) 344 | uix-html (uixc/render-to-static-markup [f])] 345 | (is (= react-html uix-html) (diff react-html uix-html))))))) 346 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /core/src/uix/compiler/alpha.cljs: -------------------------------------------------------------------------------- 1 | (ns uix.compiler.alpha 2 | "Hiccup and UIx components interpreter. Based on Reagent." 3 | (:require [react :as react] 4 | [goog.object :as gobj] 5 | [uix.hooks.alpha :as hooks] 6 | [clojure.string :as str] 7 | [cljs-bean.core :as bean] 8 | [uix.lib])) 9 | 10 | (def ^:dynamic *default-compare-args* #(= (.-argv %1) (.-argv %2))) 11 | 12 | (defn unwrap-ref [-ref] 13 | (if (implements? hooks/IRef -ref) 14 | (hooks/unwrap ^not-native -ref) 15 | -ref)) 16 | 17 | (defn js-val? [x] 18 | (not (identical? "object" (goog/typeOf x)))) 19 | 20 | (defn named? [x] 21 | (or (keyword? x) 22 | (symbol? x))) 23 | 24 | (defn hiccup-tag? [x] 25 | (keyword? x)) 26 | 27 | (declare make-element) 28 | (declare as-element) 29 | (declare expand-seq) 30 | (declare convert-prop-value) 31 | (declare convert-prop-value-shallow) 32 | 33 | (def prop-name-cache #js {:class "className" 34 | :for "htmlFor" 35 | :charset "charSet"}) 36 | 37 | (def custom-prop-name-cache #js {}) 38 | 39 | (def tag-name-cache #js {}) 40 | 41 | (def transform-fns (atom #{})) 42 | 43 | (defn add-transform-fn [f] 44 | (swap! transform-fns conj f)) 45 | 46 | (def ^:private cc-regexp (js/RegExp. "-(\\w)" "g")) 47 | 48 | (defn- cc-fn [s] 49 | (str/upper-case (aget s 1))) 50 | 51 | (defn ^string dash-to-camel [^string name-str] 52 | (if (or (str/starts-with? name-str "aria-") 53 | (str/starts-with? name-str "data-")) 54 | name-str 55 | (.replace name-str cc-regexp cc-fn))) 56 | 57 | (defn cached-prop-name [k] 58 | (if (named? k) 59 | (let [name-str (-name ^not-native k)] 60 | (if-some [k' (aget prop-name-cache name-str)] 61 | k' 62 | (let [v (dash-to-camel name-str)] 63 | (aset prop-name-cache name-str v) 64 | v))) 65 | k)) 66 | 67 | (defn cached-custom-prop-name [k] 68 | (if (named? k) 69 | (let [name-str (-name ^not-native k)] 70 | (if-some [k' (aget custom-prop-name-cache name-str)] 71 | k' 72 | (let [v (dash-to-camel name-str)] 73 | (aset custom-prop-name-cache name-str v) 74 | v))) 75 | k)) 76 | 77 | (defn convert-interop-prop-value [k v] 78 | (cond 79 | (= k :style) (if (vector? v) 80 | (-reduce ^not-native v 81 | (fn [a v] 82 | (.push a (convert-prop-value-shallow v)) 83 | a) 84 | #js []) 85 | 86 | (convert-prop-value-shallow v)) 87 | (keyword? v) (-name ^not-native v) 88 | :else v)) 89 | 90 | (defn kv-conv [o k v] 91 | (gobj/set o (cached-prop-name k) (convert-prop-value v)) 92 | o) 93 | 94 | (defn kv-conv-shallow [o k v] 95 | (gobj/set o (cached-prop-name k) (convert-interop-prop-value k v)) 96 | o) 97 | 98 | (defn custom-kv-conv [o k v] 99 | (gobj/set o (cached-custom-prop-name k) (convert-prop-value v)) 100 | o) 101 | 102 | (defn try-get-key [x] 103 | ;; try catch to avoid ClojureScript peculiarity with 104 | ;; sorted-maps with keys that are numbers 105 | (try (get x :key) 106 | (catch :default e))) 107 | 108 | (defn get-key [x] 109 | (when (map? x) 110 | (try-get-key x))) 111 | 112 | (defn convert-prop-value [x] 113 | (cond 114 | (js-val? x) x 115 | (keyword? x) (-name ^not-native x) 116 | (map? x) (reduce-kv kv-conv #js {} x) 117 | (coll? x) (clj->js x) 118 | (ifn? x) #(apply x %&) 119 | :else (clj->js x))) 120 | 121 | (defn convert-prop-value-shallow [x] 122 | (if (map? x) 123 | (reduce-kv kv-conv-shallow #js {} x) 124 | x)) 125 | 126 | (defn convert-custom-prop-value [x] 127 | (cond 128 | (js-val? x) x 129 | (keyword? x) (-name ^not-native x) 130 | (map? x) (reduce-kv custom-kv-conv #js {} x) 131 | (coll? x) (clj->js x) 132 | (ifn? x) #(apply x %&) 133 | :else (clj->js x))) 134 | 135 | (defn class-names-coll [class] 136 | (let [^js/Array classes (reduce (fn [^js/Array a c] 137 | (when ^boolean c 138 | (->> (if (keyword? c) (-name ^not-native c) c) 139 | (.push a))) 140 | a) 141 | #js [] 142 | class)] 143 | (when (pos? (.-length classes)) 144 | (.join classes " ")))) 145 | 146 | (defn class-names-map [class] 147 | (let [^js/Array classes (reduce-kv (fn [^js/Array a b ^boolean c] 148 | (when c 149 | (->> (if (keyword? b) (-name ^not-native b) b) 150 | (.push a))) 151 | a) 152 | #js [] 153 | class)] 154 | (when (pos? (.-length classes)) 155 | (.join classes " ")))) 156 | 157 | (defn ^string class-names 158 | ([]) 159 | ([class] 160 | (cond 161 | ;; {c1 true c2 false} 162 | (map? class) 163 | (class-names-map class) 164 | 165 | ;; [c1 c2 c3] 166 | (or (array? class) (coll? class)) 167 | (class-names-coll class) 168 | 169 | ;; :c1 170 | (keyword? class) 171 | (-name ^not-native class) 172 | 173 | :else class)) 174 | ([a b] 175 | (if ^boolean a 176 | (if ^boolean b 177 | (str (class-names a) " " (class-names b)) 178 | (class-names a)) 179 | (class-names b))) 180 | ([a b & rst] 181 | (reduce class-names (class-names a b) rst))) 182 | 183 | (defn set-id-class 184 | "Takes the id and class from tag keyword, and adds them to the 185 | other props. Parsed tag is JS object with :id and :class properties." 186 | [props id-class] 187 | (let [id (aget id-class 1) 188 | classes ^js/Array (aget id-class 2)] 189 | (cond-> props 190 | ;; Only use ID from tag keyword if no :id in props already 191 | (and (some? id) (nil? (get props :id))) 192 | (assoc :id id) 193 | 194 | ;; Merge classes 195 | (and (some? classes) (pos? (.-length classes))) 196 | (assoc :class (class-names classes (get props :class)))))) 197 | 198 | (defn convert-props [props id-class ^boolean shallow?] 199 | (let [class (get props :class) 200 | props (-> props 201 | (cond-> class (assoc :class (class-names class))) 202 | (set-id-class id-class))] 203 | (cond 204 | ^boolean (aget id-class 3) 205 | (convert-custom-prop-value props) 206 | 207 | shallow? 208 | (convert-prop-value-shallow props) 209 | 210 | :else (convert-prop-value props)))) 211 | 212 | (def re-tag #"[#.]?[^#.]+") 213 | 214 | (defn parse-tag [tag] 215 | (loop [matches ^js/Array (uix.lib/re-seq* re-tag tag) 216 | tag "div" 217 | id nil 218 | ^js/Array classes #js []] 219 | (let [val (aget matches 0) 220 | nval (.slice matches 1)] 221 | (if ^boolean val 222 | (cond 223 | (identical? (aget val 0) "#") 224 | (recur nval tag (.slice val 1) classes) 225 | 226 | (identical? (aget val 0) ".") 227 | (recur nval tag id (.concat classes #js [(.slice val 1)])) 228 | 229 | :else (recur nval val id classes)) 230 | #js [tag id classes (str/includes? tag "-")])))) 231 | 232 | (defn cached-parse [x] 233 | (if-some [s (aget tag-name-cache x)] 234 | s 235 | (let [v (parse-tag x)] 236 | (aset tag-name-cache x v) 237 | v))) 238 | 239 | (defn key-from-vec [v] 240 | (if-some [k (get-key (-meta v))] 241 | k 242 | (get-key (-nth v 1 nil)))) 243 | 244 | (defn native-element [parsed ^not-native argv first-el] 245 | (let [component (aget parsed 0) 246 | props (-nth argv first-el nil) 247 | props? (or (nil? props) (map? props)) 248 | props (if props? 249 | (reduce (fn [p f] (f p)) props @transform-fns) 250 | props) 251 | js-props (or ^boolean (convert-props (when props? props) parsed false) 252 | #js {}) 253 | first-child (+ first-el (if props? 1 0))] 254 | (when-some [key (get-key (-meta argv))] 255 | (gobj/set js-props "key" key)) 256 | (when-some [-ref (unwrap-ref (get props :ref))] 257 | (gobj/set js-props "ref" -ref)) 258 | (make-element argv component js-props first-child))) 259 | 260 | (defn fragment-element [^not-native argv] 261 | (let [props (-nth argv 1 nil) 262 | props? (or (nil? props) (map? props)) 263 | js-props (or ^boolean (convert-prop-value (when props? props)) 264 | #js {}) 265 | first-child (+ 1 (if props? 1 0))] 266 | (when-some [key (key-from-vec argv)] 267 | (gobj/set js-props "key" key)) 268 | (make-element argv react/Fragment js-props first-child))) 269 | 270 | (defn suspense-element [^not-native argv] 271 | (let [props (-nth argv 1 nil) 272 | props? (or (nil? props) (map? props)) 273 | [fallback props] (if props? 274 | [(as-element (get props :fallback)) 275 | (dissoc props :fallback)] 276 | [nil props]) 277 | js-props (or ^boolean (convert-prop-value (when props? props)) 278 | #js {}) 279 | first-child (+ 1 (if props? 1 0))] 280 | (when ^boolean fallback 281 | (gobj/set js-props "fallback" fallback)) 282 | (when-some [key (key-from-vec argv)] 283 | (gobj/set js-props "key" key)) 284 | (make-element argv react/Suspense js-props first-child))) 285 | 286 | (defn portal-element [^not-native argv] 287 | (.warn js/console "React portal Hiccup syntax :-> is deprecated, use uix.dom.alpha/create-portal instead") 288 | (let [child (-nth argv 1 nil) 289 | target (-nth argv 2 nil) 290 | node (if (or (string? target) (keyword? target)) 291 | (.querySelector js/document (name target)) 292 | target)] 293 | (js/ReactDOM.createPortal (as-element child) node))) 294 | 295 | (defn interop-element [^not-native argv] 296 | (let [tag (-nth argv 1 nil) 297 | parsed #js [tag nil nil] 298 | first-el 2 299 | props (-nth argv first-el nil) 300 | props? (or (nil? props) (map? props)) 301 | props (if props? 302 | (reduce (fn [p f] (f p)) props @transform-fns) 303 | props) 304 | js-props (or ^boolean (convert-props (when props? props) parsed true) 305 | #js {}) 306 | first-child (+ first-el (if props? 1 0))] 307 | (when-some [key (get-key (-meta argv))] 308 | (gobj/set js-props "key" key)) 309 | (when-some [-ref (unwrap-ref (get props :ref))] 310 | (gobj/set js-props "ref" -ref)) 311 | (make-element argv tag js-props first-child))) 312 | 313 | (defn cached-react-fn [f] 314 | (if ^boolean (.-compiled? f) 315 | (.-cljsReactCompiled f) 316 | (.-cljsReact f))) 317 | 318 | (defn cache-react-fn [f rf] 319 | (if ^boolean (.-compiled? f) 320 | (set! (.-cljsReactCompiled f) rf) 321 | (set! (.-cljsReact f) rf))) 322 | 323 | (defn symbol-for [s] 324 | (js* "Symbol.for(~{})" s)) 325 | 326 | (def lazy-sym (symbol-for "react.lazy")) 327 | (def memo-sym (symbol-for "react.memo")) 328 | 329 | (defn lazy? [t] 330 | (identical? lazy-sym (aget t "$$typeof"))) 331 | 332 | (defn memo? [t] 333 | (identical? memo-sym (aget t "$$typeof"))) 334 | 335 | (defn react-type? [t] 336 | (or (lazy? t) (memo? t))) 337 | 338 | (defn default-format-display-name [^string s] 339 | (let [^js/Array parts (.split s #"\$") 340 | last-idx (dec ^number (.-length parts)) 341 | ^string name-part (aget parts last-idx)] 342 | (if (== 1 (.-length parts)) 343 | (demunge name-part) 344 | (-> ^js/Array (.slice parts 0 last-idx) 345 | ^string (.join ".") 346 | (str "/" name-part) 347 | demunge)))) 348 | 349 | 350 | (def ^:dynamic *format-display-name* default-format-display-name) 351 | 352 | (defn format-display-name [s] 353 | (if (fn? *format-display-name*) 354 | (*format-display-name* s) 355 | (throw (ex-info "unexpected uix.compiler.alpha/*format-display-name* is not bound to a function" 356 | {:bound-value *format-display-name* 357 | :value-type (goog/typeOf *format-display-name*)})))) 358 | 359 | (defn effective-component-name [^js f] 360 | (or (when-some [display-name (.-displayName f)] 361 | (if (string? display-name) 362 | display-name)) 363 | (when-some [name (.-name f)] 364 | (if (string? name) 365 | name)))) 366 | 367 | (defn with-name [^js f ^js rf ^js rf-memo] 368 | (when-let [component-name (effective-component-name f)] 369 | (when-some [display-name (format-display-name component-name)] 370 | (set! (.-displayName rf) display-name) 371 | (when-not ^boolean (.-uix-no-memo f) 372 | (set! (.-displayName rf-memo) (str "memo(" display-name ")")))))) 373 | 374 | (defn fn-to-react-fn [^js f] 375 | (if (react-type? f) 376 | f 377 | (let [rf #(let [argv ^not-native (.-argv %)] 378 | (as-element (apply (-nth argv 0) (subvec argv 1)))) 379 | rf-memo (if-not ^boolean (.-uix-no-memo f) 380 | (react/memo rf *default-compare-args*) 381 | rf)] 382 | (when (and ^boolean goog.DEBUG (exists? js/__REACT_DEVTOOLS_GLOBAL_HOOK__)) 383 | (set! (.-uixf rf) f)) 384 | (when ^boolean goog.DEBUG 385 | (with-name f rf rf-memo)) 386 | (cache-react-fn f rf-memo) 387 | rf-memo))) 388 | 389 | (defn as-lazy-component [f] 390 | (if-some [cached-fn (cached-react-fn f)] 391 | cached-fn 392 | (let [rf #(as-element (apply f (subvec (.-argv %) 1))) 393 | rf-memo (if-not ^boolean (.-uix-no-memo f) 394 | (react/memo rf *default-compare-args*) 395 | rf)] 396 | (when (and ^boolean goog.DEBUG (exists? js/__REACT_DEVTOOLS_GLOBAL_HOOK__)) 397 | (set! (.-uixf rf) f)) 398 | (when ^boolean goog.DEBUG 399 | (with-name f rf rf-memo)) 400 | (cache-react-fn f rf-memo) 401 | rf-memo))) 402 | 403 | (defn as-component [tag] 404 | (if-some [cached-fn (cached-react-fn tag)] 405 | cached-fn 406 | (fn-to-react-fn tag))) 407 | 408 | (defn as-react [f] 409 | #(as-element (f (bean/bean %)))) 410 | 411 | (defn component-element [tag v] 412 | (let [js-props #js {}] 413 | (set! (.-argv js-props) v) 414 | (when-some [key (key-from-vec v)] 415 | (gobj/set js-props "key" key)) 416 | (react/createElement (as-component tag) js-props))) 417 | 418 | (defn vec-to-elem [^not-native v] 419 | (let [tag (-nth v 0 nil)] 420 | (cond 421 | (keyword-identical? :<> tag) (fragment-element v) 422 | (keyword-identical? :# tag) (suspense-element v) 423 | (keyword-identical? :-> tag) (portal-element v) 424 | (keyword-identical? :> tag) (interop-element v) 425 | (hiccup-tag? tag) (-> (cached-parse (-name ^not-native tag)) 426 | (native-element v 1)) 427 | :else (component-element tag v)))) 428 | 429 | (defn as-element [x] 430 | (cond 431 | (js-val? x) x 432 | (vector? x) (vec-to-elem x) 433 | (seq? x) (expand-seq x) 434 | (keyword? x) (-name ^not-native x) 435 | (satisfies? IPrintWithWriter x) (pr-str x) 436 | :else x)) 437 | 438 | (defn expand-seq [s] 439 | (reduce (fn [ret e] 440 | (.push ret (as-element e)) 441 | ret) 442 | #js [] 443 | s)) 444 | 445 | (defn make-element [^not-native argv component js-props first-child] 446 | (case (- (-count argv) first-child) 447 | 0 (react/createElement component js-props) 448 | 1 (->> (as-element (-nth argv first-child nil)) 449 | (react/createElement component js-props)) 450 | (.apply react/createElement nil 451 | (reduce-kv (fn [^js/Array a k v] 452 | (when (>= k first-child) 453 | (.push a (as-element v))) 454 | a) 455 | #js [component js-props] 456 | argv)))) 457 | --------------------------------------------------------------------------------