0.11.3 updated docstrings

This commit is contained in:
Nikita Prokopov
2018-11-28 17:58:57 +03:00
parent 1dcbabb6d4
commit d615089e32
13 changed files with 582 additions and 179 deletions

View File

@ -1,3 +1,7 @@
## 0.11.3
- Docstrings for https://cljdoc.org/d/rum/rum
## 0.11.2
- Server-render on-* event handlers with string values

6
doc/cljdoc.edn Normal file
View File

@ -0,0 +1,6 @@
{:cljdoc.doc/tree [
["Readme" {:file "README.md"}]
["Changelog" {:file "CHANGELOG.md"}]
["Useful mixins" {:file "doc/useful-mixins.md"}]
["React interop" {:file "doc/react-interop.md"}]
]}

109
doc/react-interop.md Normal file
View File

@ -0,0 +1,109 @@
# Use different React version
Add to `project.clj`:
```
:dependencies {
[rum "0.11.3" :exclusions [[cljsjs/react] [cljsjs/react-dom]]]
[cljsjs/react "16.6.0-0"]
[cljsjs/react-dom "16.6.0-0"]
}
```
# Including React.js manually
If you want to include `react.js` yourself, then add this to `project.clj`:
```
:dependencies {
[rum "0.11.3" :exclusions [[cljsjs/react] [cljsjs/react-dom]]]
}
```
Create two files
1. `src/cljsjs/react.cljs`:
```
(ns cljsjs.react)
```
2. `src/cljsjs/react/dom.cljs`:
```
(ns cljsjs.react.dom)
```
Add to your HTML the version you want:
```
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
```
# Using React with addons
```clj
[rum "0.11.3" :exclusions [cljsjs/react cljsjs/react-dom]]
[cljsjs/react-dom "16.2.0-3" :exclusions [cljsjs/react]]
[cljsjs/react-dom-server "16.2.0-3" :exclusions [cljsjs/react]]
[cljsjs/react-with-addons "16.2.0-3"]
```
# Profiling with [React perf](https://facebook.github.io/react/docs/perf.html)
Specify the `react-with-addons` dependency in your `project.clj` (see above ↑)
Then from within your program run:
```clj
(js/React.addons.Perf.start)
;;run your app
(js/React.addons.Perf.stop)
(js/React.addons.Perf.printWasted)
```
and results will be printed to the developer console.
# Using 3rd-party React components
Given e.g. [react-router-transition](https://github.com/maisano/react-router-transition)
```clj
(defn route-transition [pathname children]
(js/React.createElement js/RouteTransition
#js { :pathname pathname
:atEnter #js { :opacity 0 }
:atLeave #js { opacity: 0 }
:atActive #js { opacity: 1 } }
(clj->js children)))
```
Another example [react-component/slider](https://github.com/react-component/slider)
```clj
(defn range-slider [min max]
(js/React.createElement js/Slider #js { :min min
:max max
:range true
:defaultValue #js [40 60] }))
```
**Note:** See how `defn` is used here instead of `defc`? Using `defc` would cause two components being created (e.g. `range-slider` and the `Slider` component). Because in many cases you don't need the wrapping component you can just use `defn`.
# Get displayName of component
This might be useful for development when you want to know which component this mixin is handling:
```clojure
(ns ...
(:require [goog.object :as gobj]))
(defn display-name
"Returns the displayname of the component"
[state]
(gobj/getValueByKeys
(:rum/react-component state)
"constructor"
"displayName"))
```

184
doc/useful-mixins.md Normal file
View File

@ -0,0 +1,184 @@
# Request stuff on mount by AJAX
Components using this mixin will do an AJAX request on mount and will update themselves when they got a reply. Mixin puts an atom to the state whose value (after deref) is either nil (request pending) or returned value.
```clojure
(defn ajax-mixin [url key]
{ :will-mount
(fn [state]
(let [*data (atom nil)
comp (:rum/react-component state)]
(ajax
url
(fn [data]
(reset! *data data)
(rum/request-render comp)))
(assoc state key *data))) })
(rum/defcs user-info < (ajax-mixin "/api/user/info" ::user)
[state]
(if-let [user @(::user state)]
...
[:div "Loading..."]))
```
Customize to your taste: dynamic URL generation from component args, AJAX retries, failed state, callback on finish, deserialization.
# Debouncer
```clojure
(ns ... (:import [goog.async Debouncer]))
(defn debouncer-mixin
"Creates a debouncer in (:debouncer state) which can be called with (.fire).
Invokes the callback cb or invokes the first argument passed to fire.
Usage:
1. (debouncer-mixin 200)
(.fire (:debouncer state) #(do-actual-action ...))
2. (debouncer-mixin 200 #(do-an-action ...))
(.fire (:debouncer state))"
([ms] (debouncer-mixin ms nil))
([ms cb]
{:will-mount
(fn debouncer-mount [state]
(assoc state :debouncer (Debouncer. (if (nil? cb) #(%1) cb) ms)))
:will-unmount
(fn debouncer-unmount [state]
(.dispose (:debouncer state))
state)}))
```
# Install CSS styles on mount
For your root rum component, you can install CSS styles on mount and uninstall them on unmount.
This is useful if you have your (garden) styles defined in `cljc` file. In production you generate
a css file and include it with a normal `<style>` tag, in development however, you can just use this
mixin to have them automatically be applied as soon as you change any style.
```clojure
(ns ...
(:require [goog.style :as gstyle]))
(defn install-styles-mixin
"Installes the stylesheet when mounting. Uninstall when unmount.
Useful for use at the very root render and use with garden in a cljc environment.
Live update of CSS without needing to go trough figwheel :)"
[css-str]
{:will-mount
(fn [st]
(assoc st ::stylesheet (gstyle/installStyles css-str)))
:will-unmount
(fn [st]
(gstyle/uninstallStyles (::stylesheet st))
st)})
;; Then use them like so:
app < (install-styles-mixin (your-cljc/gen-css))
```
# Measure render
You can measure how long a component needs to render with this mixin:
```clojure
(defn perf-measure-mixin
[desc]
"Does performance measurements in development."
{:wrap-render
(fn wrap-render [render-fn]
(fn [state]
(profile
(str "Render " desc)
(render-fn state))))})
;; where profile is a macro like this:
(defmacro profile [k & body]
(if macros/dev?
`(let [k# ~k]
(.time js/console k#)
(let [res# (do ~@body)]
(.timeEnd js/console k#)
res#))
`(do ~@body)))
```
# Keyboard shortcut
Install a keyboard shortcut that is only valid while the component is mounted:
```clojure
(defn keyboard-mixin
"Triggers f when key is pressed while the component is mounted.
if target is a function it will be called AFTER the component mounted
with state and should return a dom node that is the target of the listener.
If no target is given it is defaulted to js/window (global handler)
Ex:
(keyboard-mixin \"esc\" #(browse-to :home/home))"
([key f] (keyboard-mixin key f js/window))
([key f target]
(let [target-fn (if (fn? target) target (fn [_] target))]
{:did-mount
(fn [state]
(assoc state ::keyboard-listener
(keyboard/install-shortcut! key f false (target-fn state))))
:will-unmount
(fn [state]
((::keyboard-listener state))
state)})))
;; where install-shortcut! is:
(ns you-tools.keyboard
(:require [goog.events :as events]
[goog.ui.KeyboardShortcutHandler.EventType :as EventType]
[goog.events.KeyCodes :as KeyCodes])
(:import [goog.ui KeyboardShortcutHandler]))
(defn install-shortcut!
"Installs a Keyboard Shortcut handler.
The key is a string the trigger is a function that will receive the keyboard event as the
first argument. If once? is true the keyboard shortcut is only fired once.
The unregister handler is returned and can be called to unregister the listener.
If target is not given it's attached to window."
([key trigger] (install-shortcut! key trigger false js/window))
([key trigger once?] (install-shortcut! key trigger once? js/window))
([key trigger once? target]
(let [handler (new KeyboardShortcutHandler target)]
(.registerShortcut handler (str key once?) key)
(events/listen
handler
EventType/SHORTCUT_TRIGGERED
(fn [e]
(trigger e)
(when once?
(.unregisterShortcut handler keys))))
(fn []
(.unregisterShortcut handler key)))))
```
# Remember state
This mixin can be used to remember the state of a component. For this to work you need to UNIQUELY identify
your component somehow. For instance, by generating some unique id from the given args to your component:
```clojure
(defn remember-state-mixin
"Remembers the state :rum/local for a given component and swaps it back on mount.
The given function is passed the args of the component (with apply).
And should return a map key that is used to uniquely identify the component.
(remember-state-mixin (fn [arg1 arg2] (:db/id arg1)))"
[f]
(let [store (atom {})
key-fn (fn [state] (apply f (:rum/args state)))]
{:will-unmount
(fn remember-state-unmount [state]
(swap! store assoc (key-fn state) @(:rum/local state))
state)
:will-mount
(fn remember-state-mount [state]
(let [old-state @store
key (key-fn state)]
(swap! (:rum/local state) merge (get old-state key {})))
state)}))
```

View File

@ -1,4 +1,4 @@
(defproject rum "0.11.2"
(defproject rum "0.11.3"
:description "ClojureScript wrapper for React"
:license { :name "Eclipse"
:url "http://www.eclipse.org/legal/epl-v10.html" }

View File

@ -63,7 +63,11 @@
(defmacro defc
"Defc does couple of things:
"```
(defc name doc-string? (< mixins+)? [ params* ] render-body+)
```
Defc does couple of things:
1. Wraps body into sablono/compile-html
2. Generates render function from that
@ -71,37 +75,38 @@
4. Using that class, generates constructor fn [args]->ReactElement
5. Defines top-level var with provided name and assigns ctor to it
(rum/defc label [t]
[:div t])
;; creates React class
;; defines ctor fn (defn label [t] ...) => element
(label \"text\") ;; => returns React element built with label class
Usage:
```
(rum/defc label < rum/static [t]
[:div t])
(defc name doc-string? [< mixins+]? [params*] render-body+)"
;; creates React class
;; adds mixin rum/static
;; defines ctor fn (defn label [t] ...) => element
(label \"text\") ;; => returns React element built with label class
```"
[& body]
(-defc 'rum.core/build-defc (boolean (:ns &env)) body))
(defmacro defcs
"Same as defc, but render will take additional first argument: state
Usage:
(defcs name doc-string? [< mixins+]? [state params*] render-body+)"
"```
(defcs name doc-string? (< mixins+)? [ state-arg params* ] render-body+)
```
Same as [[defc]], but render will take additional first argument: component state."
[& body]
(-defc 'rum.core/build-defcs (boolean (:ns &env)) body))
(defmacro defcc
"Same as defc, but render will take additional first argument: react component
Usage:
"```
(defcc name doc-string? (< mixins+)? [ comp-arg params* ] render-body+)
```
(defcc name doc-string? [< mixins+]? [comp params*] render-body+)"
Same as [[defc]], but render will take additional first argument: react component."
[& body]
(-defc 'rum.core/build-defcc (boolean (:ns &env)) body))
@ -127,19 +132,19 @@
(or dom [:rum/nothing])))))
(defn build-defc [render-body mixins display-name]
(defn ^:no-doc build-defc [render-body mixins display-name]
(if (empty? mixins)
(fn [& args] (or (apply render-body args) [:rum/nothing]))
(let [render (fn [state] [(apply render-body (:rum/args state)) state])]
(build-ctor render mixins display-name))))
(defn build-defcs [render-body mixins display-name]
(defn ^:no-doc build-defcs [render-body mixins display-name]
(let [render (fn [state] [(apply render-body state (:rum/args state)) state])]
(build-ctor render mixins display-name)))
(defn build-defcc [render-body mixins display-name]
(defn ^:no-doc build-defcc [render-body mixins display-name]
(let [render (fn [state] [(apply render-body (:rum/react-component state) (:rum/args state)) state])]
(build-ctor render mixins display-name)))
@ -147,7 +152,17 @@
;; rum.core APIs
(defn with-key [element key]
(defn with-key
"Adds React key to element.
```
(rum/defc label [text] [:div text])
(-> (label)
(rum/with-key \"abc\")
(rum/mount js/document.body))
```"
[element key]
(cond
(render/nothing? element)
element
@ -159,43 +174,65 @@
(into [(first element) {:key key}] (next element))))
(defn with-ref [element ref]
(defn with-ref
"Supported, does nothing."
[element ref]
element)
;; mixins
(def static {})
(def static "Supported, does nothing." {})
(defn local
"Mixin constructor. Adds an atom to components state that can be used to keep stuff during components lifecycle. Component will be re-rendered if atoms value changes. Atom is stored under user-provided key or under `:rum/local` by default.
```
(rum/defcs counter < (rum/local 0 :cnt)
[state label]
(let [*cnt (:cnt state)]
[:div {:on-click (fn [_] (swap! *cnt inc))}
label @*cnt]))
(rum/mount (counter \"Click count: \"))
```"
([initial] (local initial :rum/local))
([initial key]
{:will-mount (fn [state]
(assoc state key (atom initial)))}))
(def reactive {})
(def reactive "Supported, does nothing." {})
(def react deref)
(def ^{:arglists '([ref])
:doc "Supported as simple deref."}
react deref)
(defn cursor-in
"Given atom with deep nested value and path inside it, creates an atom-like structure
that can be used separately from main atom, but will sync changes both ways:
(def db (atom { :users { \"Ivan\" { :age 30 }}}))
(def ivan (rum/cursor db [:users \"Ivan\"]))
\\@ivan ;; => { :age 30 }
(swap! ivan update :age inc) ;; => { :age 31 }
\\@db ;; => { :users { \"Ivan\" { :age 31 }}}
(swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}}
\\@ivan ;; => { :age 32 }
```
(def db (atom { :users { \"Ivan\" { :age 30 }}}))
(def ivan (rum/cursor db [:users \"Ivan\"]))
(deref ivan) ;; => { :age 30 }
(swap! ivan update :age inc) ;; => { :age 31 }
(deref db) ;; => { :users { \"Ivan\" { :age 31 }}}
(swap! db update-in [:users \"Ivan\" :age] inc)
;; => { :users { \"Ivan\" { :age 32 }}}
(deref ivan) ;; => { :age 32 }
```
Returned value supports deref, swap!, reset!, watches and metadata.
The only supported option is `:meta`"
Returned value supports `deref`, `swap!`, `reset!`, watches and metadata.
The only supported option is `:meta`"
^rum.cursor.Cursor [ref path & { :as options }]
(if (instance? Cursor ref)
(cursor/Cursor. (.-ref ^Cursor ref) (into (.-path ^Cursor ref) path) (:meta options) (volatile! {}))
@ -203,90 +240,93 @@
(defn cursor
"Same as `rum.core/cursor-in` but accepts single key instead of path vector"
"Same as [[cursor-in]] but accepts single key instead of path vector."
^rum.cursor.Cursor [ref key & options]
(apply cursor-in ref [key] options))
(def ^{:style/indent 2} derived-atom
"Use this to create “chains” and acyclic graphs of dependent atoms.
`derived-atom` will:
- Take N “source” refs
- Set up a watch on each of them
- Create “sink” atom
- When any of source refs changes:
- re-run function `f`, passing N dereferenced values of source refs
- `reset!` result of `f` to the sink atom
- return sink atom
(def *a (atom 0))
(def *b (atom 1))
(def *x (derived-atom [*a *b] ::key
(fn [a b]
(str a \":\" b))))
(type *x) ;; => clojure.lang.Atom
\\@*x ;; => 0:1
(swap! *a inc)
\\@*x ;; => 1:1
(reset! *b 7)
\\@*x ;; => 1:7
(def ^{:style/indent 2
:arglists '([refs key f] [refs key f opts])
:doc "Use this to create “chains” and acyclic graphs of dependent atoms.
[[derived-atom]] will:
- Take N “source” refs.
- Set up a watch on each of them.
- Create “sink” atom.
- When any of source refs changes:
- re-run function `f`, passing N dereferenced values of source refs.
- `reset!` result of `f` to the sink atom.
- Return sink atom.
Arguments:
refs - sequence of source refs
key - unique key to register watcher, see `clojure.core/add-watch`
f - function that must accept N arguments (same as number of source refs)
and return a value to be written to the sink ref.
Note: `f` will be called with already dereferenced values
opts - optional. Map of:
:ref - Use this as sink ref. By default creates new atom
:check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`.
If result of `f` is equal to the old one, do not call `reset!`.
Defaults to `true`. Set to false if calling `=` would be expensive"
derived-atom/derived-atom)
Example:
```
(def *a (atom 0))
(def *b (atom 1))
(def *x (derived-atom [*a *b] ::key
(fn [a b]
(str a \":\" b))))
(type *x) ;; => clojure.lang.Atom
(deref *x) ;; => \"0:1\"
(swap! *a inc)
(deref *x) ;; => \"1:1\"
(reset! *b 7)
(deref *x) ;; => \"1:7\"
```
Arguments:
- `refs` - sequence of source refs,
- `key` - unique key to register watcher, same as in `clojure.core/add-watch`,
- `f` - function that must accept N arguments (same as number of source refs) and return a value to be written to the sink ref. Note: `f` will be called with already dereferenced values,
- `opts` - optional. Map of:
- `:ref` - use this as sink ref. By default creates new atom,
- `:check-equals?` - Defaults to `true`. If equality check should be run on each source update: `(= @sink (f new-vals))`. When result of recalculating `f` equals to the old value, `reset!` wont be called. Set to `false` if checking for equality can be expensive."}
derived-atom derived-atom/derived-atom)
;;; Server-side rendering
(def ^{:arglists '([element] [element opts])
:doc "Main server-side rendering method. Given component, returns HTML string with static markup of that component. Serve that string to the browser and [[mount]] same Rum component over it. React will be able to reuse already existing DOM and will initialize much faster. No opts are supported at the moment."}
render-html render/render-html)
(def render-html
"Main server-side rendering method. Given component, returns HTML string with
static markup of that component. Serve that string to the browser and
`rum.core/mount` same Rum component over it. React will be able to reuse already
existing DOM and will initialize much faster"
render/render-html)
(def render-static-markup
"Same as `rum.core/render-html` but returned string has nothing React-specific.
This allows Rum to be used as traditional server-side template engine"
render/render-static-markup)
(def ^{:arglists '([element])
:doc "Same as [[render-html]] but returned string has nothing React-specific. This allows Rum to be used as traditional server-side templating engine."}
render-static-markup render/render-static-markup)
;; method parity with CLJS version so you can avoid conditional directive
;; in e.g. did-mount/will-unmount mixin bodies
(defn state [c]
(defn ^:no-doc state [c]
(throw (UnsupportedOperationException. "state is only available from ClojureScript")))
(defn dom-node [s]
(defn ^:no-doc dom-node [s]
(throw (UnsupportedOperationException. "dom-node is only available from ClojureScript")))
(defn ref [s k]
(defn ^:no-doc ref [s k]
(throw (UnsupportedOperationException. "ref is only available from ClojureScript")))
(defn ref-node [s k]
(defn ^:no-doc ref-node [s k]
(throw (UnsupportedOperationException. "ref is only available from ClojureScript")))
(defn mount [c n]
(defn ^:no-doc mount [c n]
(throw (UnsupportedOperationException. "mount is only available from ClojureScript")))
(defn unmount [c]
(defn ^:no-doc unmount [c]
(throw (UnsupportedOperationException. "unmount is only available from ClojureScript")))
(defn request-render [c]
(defn ^:no-doc request-render [c]
(throw (UnsupportedOperationException. "request-render is only available from ClojureScript")))

View File

@ -12,12 +12,12 @@
(defn state
"Given React component, returns Rum state associated with it"
"Given React component, returns Rum state associated with it."
[comp]
(gobj/get (.-state comp) ":rum/state"))
(defn extend! [obj props]
(defn- extend! [obj props]
(doseq [[k v] props
:when (some? v)]
(gobj/set obj (name k) (clj->js v))))
@ -148,7 +148,7 @@
(with-meta ctor { :rum/class class })))
(defn build-defc [render-body mixins display-name]
(defn ^:no-doc build-defc [render-body mixins display-name]
(if (empty? mixins)
(let [class (fn [props]
(apply render-body (aget props ":rum/args")))
@ -160,12 +160,12 @@
(build-ctor render mixins display-name))))
(defn build-defcs [render-body mixins display-name]
(defn ^:no-doc build-defcs [render-body mixins display-name]
(let [render (fn [state] [(apply render-body state (:rum/args state)) state])]
(build-ctor render mixins display-name)))
(defn build-defcc [render-body mixins display-name]
(defn ^:no-doc build-defcc [render-body mixins display-name]
(let [render (fn [state] [(apply render-body (:rum/react-component state) (:rum/args state)) state])]
(build-ctor render mixins display-name)))
@ -204,7 +204,7 @@
(defn request-render
"Schedules react component to be rendered on next animation frame"
"Schedules react component to be rendered on next animation frame."
[component]
(when (empty? @render-queue)
(schedule render))
@ -212,58 +212,74 @@
(defn mount
"Add component to the DOM tree. Idempotent. Subsequent mounts will just update component"
[component node]
(js/ReactDOM.render component node)
"Add element to the DOM tree. Idempotent. Subsequent mounts will just update element."
[element node]
(js/ReactDOM.render element node)
nil)
(defn unmount
"Removes component from the DOM tree"
"Removes component from the DOM tree."
[node]
(js/ReactDOM.unmountComponentAtNode node))
(defn hydrate
"Hydrates server rendered DOM tree with provided component."
[component node]
(js/ReactDOM.hydrate component node))
"Same as [[mount]] but must be called on DOM tree already rendered by a server via [[render-html]]."
[element node]
(js/ReactDOM.hydrate element node))
(defn portal
"Render `component` in a DOM `node` that might be ouside of current DOM hierarchy"
[component node]
(js/ReactDOM.createPortal component node))
"Render `element` in a DOM `node` that is ouside of current DOM hierarchy."
[element node]
(js/ReactDOM.createPortal element node))
;; initialization
(defn with-key
"Adds React key to component"
[component key]
(js/React.cloneElement component #js { "key" key } nil))
"Adds React key to element.
```
(rum/defc label [text] [:div text])
(-> (label)
(rum/with-key \"abc\")
(rum/mount js/document.body))
```"
[element key]
(js/React.cloneElement element #js { "key" key } nil))
(defn with-ref
"Adds React ref (string or callback) to component"
[component ref]
(js/React.cloneElement component #js { "ref" ref } nil))
"Adds React ref (string or callback) to element.
```
(rum/defc label [text] [:div text])
(-> (label)
(rum/with-ref \"abc\")
(rum/mount js/document.body))
```"
[element ref]
(js/React.cloneElement element #js { "ref" ref } nil))
(defn dom-node
"Given state, returns top-level DOM node. Cant be called during render"
"Given state, returns top-level DOM node of component. Call it during lifecycle callbacks. Cant be called during render."
[state]
(js/ReactDOM.findDOMNode (:rum/react-component state)))
(defn ref
"Given state and ref handle, returns React component"
"Given state and ref handle, returns React component."
[state key]
(-> state :rum/react-component (aget "refs") (aget (name key))))
(defn ref-node
"Given state and ref handle, returns DOM node associated with ref"
"Given state and ref handle, returns DOM node associated with ref."
[state key]
(js/ReactDOM.findDOMNode (ref state (name key))))
@ -271,8 +287,21 @@
;; static mixin
(def static
"Mixin. Will avoid re-render if none of components arguments have changed.
Does equality check (=) on all arguments"
"Mixin. Will avoid re-render if none of components arguments have changed. Does equality check (`=`) on all arguments.
```
(rum/defc label < rum/static
[text]
[:div text])
(rum/mount (label \"abc\") js/document.body)
;; def != abc, will re-render
(rum/mount (label \"def\") js/document.body)
;; def == def, wont re-render
(rum/mount (label \"def\") js/document.body)
```"
{ :should-update
(fn [old-state new-state]
(not= (:rum/args old-state) (:rum/args new-state))) })
@ -281,9 +310,17 @@
;; local mixin
(defn local
"Mixin constructor. Adds an atom to components state that can be used to keep stuff
during components lifecycle. Component will be re-rendered if atoms value changes.
Atom is stored under user-provided key or under `:rum/local` by default"
"Mixin constructor. Adds an atom to components state that can be used to keep stuff during components lifecycle. Component will be re-rendered if atoms value changes. Atom is stored under user-provided key or under `:rum/local` by default.
```
(rum/defcs counter < (rum/local 0 :cnt)
[state label]
(let [*cnt (:cnt state)]
[:div {:on-click (fn [_] (swap! *cnt inc))}
label @*cnt]))
(rum/mount (counter \"Click count: \"))
```"
([initial] (local initial :rum/local))
([initial key]
{ :will-mount
@ -302,7 +339,17 @@
(def reactive
"Mixin. Works in conjunction with `rum.core/react`"
"Mixin. Works in conjunction with [[react]].
```
(rum/defc comp < rum/reactive
[*counter]
[:div (rum/react counter)])
(def *counter (atom 0))
(rum/mount (comp *counter) js/document.body)
(swap! *counter inc) ;; will force comp to re-render
```"
{ :init
(fn [state props]
(assoc state :rum.reactive/key (random-uuid)))
@ -333,9 +380,7 @@
(defn react
"Works in conjunction with `rum.core/reactive` mixin. Use this function instead of
`deref` inside render, and your component will subscribe to changes happening
to the derefed atom."
"Works in conjunction with [[reactive]] mixin. Use this function instead of `deref` inside render, and your component will subscribe to changes happening to the derefed atom."
[ref]
(assert *reactions* "rum.core/react is only supported in conjunction with rum.core/reactive")
(vswap! *reactions* conj ref)
@ -344,41 +389,48 @@
;; derived-atom
(def ^{:style/indent 2} derived-atom
"Use this to create “chains” and acyclic graphs of dependent atoms.
`derived-atom` will:
- Take N “source” refs
- Set up a watch on each of them
- Create “sink” atom
- When any of source refs changes:
- re-run function `f`, passing N dereferenced values of source refs
- `reset!` result of `f` to the sink atom
- return sink atom
(def ^{:style/indent 2
:arglists '([refs key f] [refs key f opts])
:doc "Use this to create “chains” and acyclic graphs of dependent atoms.
[[derived-atom]] will:
- Take N “source refs.
- Set up a watch on each of them.
- Create sink atom.
- When any of source refs changes:
- re-run function `f`, passing N dereferenced values of source refs.
- `reset!` result of `f` to the sink atom.
- Return sink atom.
(def *a (atom 0))
(def *b (atom 1))
(def *x (derived-atom [*a *b] ::key
(fn [a b]
(str a \":\" b))))
(type *x) ;; => clojure.lang.Atom
\\@*x ;; => 0:1
(swap! *a inc)
\\@*x ;; => 1:1
(reset! *b 7)
\\@*x ;; => 1:7
Example:
Arguments:
refs - sequence of source refs
key - unique key to register watcher, see `clojure.core/add-watch`
f - function that must accept N arguments (same as number of source refs)
and return a value to be written to the sink ref.
Note: `f` will be called with already dereferenced values
opts - optional. Map of:
:ref - Use this as sink ref. By default creates new atom
:check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`.
If result of `f` is equal to the old one, do not call `reset!`.
Defaults to `true`. Set to false if calling `=` would be expensive"
derived-atom/derived-atom)
```
(def *a (atom 0))
(def *b (atom 1))
(def *x (derived-atom [*a *b] ::key
(fn [a b]
(str a \":\" b))))
(type *x) ;; => clojure.lang.Atom
(deref *x) ;; => \"0:1\"
(swap! *a inc)
(deref *x) ;; => \"1:1\"
(reset! *b 7)
(deref *x) ;; => \"1:7\"
```
Arguments:
- `refs` - sequence of source refs,
- `key` - unique key to register watcher, same as in `clojure.core/add-watch`,
- `f` - function that must accept N arguments (same as number of source refs) and return a value to be written to the sink ref. Note: `f` will be called with already dereferenced values,
- `opts` - optional. Map of:
- `:ref` - use this as sink ref. By default creates new atom,
- `:check-equals?` - Defaults to `true`. If equality check should be run on each source update: `(= @sink (f new-vals))`. When result of recalculating `f` equals to the old value, `reset!` wont be called. Set to `false` if checking for equality can be expensive."}
derived-atom derived-atom/derived-atom)
;; cursors
@ -387,16 +439,24 @@
"Given atom with deep nested value and path inside it, creates an atom-like structure
that can be used separately from main atom, but will sync changes both ways:
(def db (atom { :users { \"Ivan\" { :age 30 }}}))
(def ivan (rum/cursor db [:users \"Ivan\"]))
\\@ivan ;; => { :age 30 }
(swap! ivan update :age inc) ;; => { :age 31 }
\\@db ;; => { :users { \"Ivan\" { :age 31 }}}
(swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}}
\\@ivan ;; => { :age 32 }
```
(def db (atom { :users { \"Ivan\" { :age 30 }}}))
(def ivan (rum/cursor db [:users \"Ivan\"]))
(deref ivan) ;; => { :age 30 }
(swap! ivan update :age inc) ;; => { :age 31 }
(deref db) ;; => { :users { \"Ivan\" { :age 31 }}}
(swap! db update-in [:users \"Ivan\" :age] inc)
;; => { :users { \"Ivan\" { :age 32 }}}
(deref ivan) ;; => { :age 32 }
```
Returned value supports deref, swap!, reset!, watches and metadata.
The only supported option is `:meta`"
Returned value supports `deref`, `swap!`, `reset!`, watches and metadata.
The only supported option is `:meta`"
[ref path & {:as options}]
(if (instance? cursor/Cursor ref)
(cursor/Cursor. (.-ref ref) (into (.-path ref) path) (:meta options))
@ -404,6 +464,6 @@
(defn cursor
"Same as `rum.core/cursor-in` but accepts single key instead of path vector"
"Same as [[cursor-in]] but accepts single key instead of path vector."
[ref key & options]
(apply cursor-in ref [key] options))

View File

@ -1,4 +1,4 @@
(ns rum.cursor)
(ns ^:no-doc rum.cursor)
(deftype Cursor [ref path ^:volatile-mutable meta watches]

View File

@ -1,4 +1,4 @@
(ns rum.cursor)
(ns ^:no-doc rum.cursor)
(deftype Cursor [ref path meta]

View File

@ -1,4 +1,4 @@
(ns rum.derived-atom)
(ns ^:no-doc rum.derived-atom)
(defn derived-atom

View File

@ -1,4 +1,4 @@
(ns rum.server-render
(ns ^:no-doc rum.server-render
(:require
[clojure.string :as str])
(:import

View File

@ -1,4 +1,4 @@
(ns rum.util)
(ns ^:no-doc rum.util)
(defn collect [key mixins]

View File

@ -360,7 +360,7 @@ e,h,f,k,l,m,n,p,q,w,z);B=C(D);var G=N(D);if(null==G)return a.Ca?a.Ca(b,c,d,e,h,f
q,w,z,B,D,G,E);S=C(ia);var Ga=N(ia);if(null==Ga)return a.Ga?a.Ga(b,c,d,e,h,f,k,l,m,n,p,q,w,z,B,D,G,E,S):a.call(a,b,c,d,e,h,f,k,l,m,n,p,q,w,z,B,D,G,E,S);ia=C(Ga);Ga=N(Ga);if(null==Ga)return a.Ha?a.Ha(b,c,d,e,h,f,k,l,m,n,p,q,w,z,B,D,G,E,S,ia):a.call(a,b,c,d,e,h,f,k,l,m,n,p,q,w,z,B,D,G,E,S,ia);b=[b,c,d,e,h,f,k,l,m,n,p,q,w,z,B,D,G,E,S,ia];for(c=Ga;;)if(c)b.push(C(c)),c=N(c);else break;return a.apply(a,b)}
function be(a,b){if(a.D){var c=a.F,d=Sd(c+1,b);return d<=c?Wd(a,d,b):a.D(b)}c=I(b);return null==c?a.v?a.v():a.call(a):Xd(a,C(c),N(c))}function ce(a,b,c){if(a.D){b=R(b,c);var d=a.F;c=Sd(d,c)+1;return c<=d?Wd(a,c,b):a.D(b)}return Xd(a,b,I(c))}function de(a,b,c,d){return a.D?(b=R(b,R(c,d)),c=a.F,d=2+Sd(c-1,d),d<=c?Wd(a,d,b):a.D(b)):Yd(a,b,c,I(d))}function ee(a,b,c,d,e){return a.D?(b=R(b,R(c,R(d,e))),c=a.F,e=3+Sd(c-2,e),e<=c?Wd(a,e,b):a.D(b)):Zd(a,b,c,d,I(e))}
function ed(a,b,c,d,e,f){return a.D?(f=Td(f),b=R(b,R(c,R(d,R(e,f)))),c=a.F,f=4+Sd(c-3,f),f<=c?Wd(a,f,b):a.D(b)):ae(a,b,c,d,e,Td(f))}
function fe(){"undefined"===typeof Ma&&(Ma=function(a){this.mc=a;this.m=393216;this.C=0},Ma.prototype.U=function(a,b){return new Ma(b)},Ma.prototype.R=function(){return this.mc},Ma.prototype.Z=function(){return!1},Ma.prototype.next=function(){return Error("No such element")},Ma.prototype.remove=function(){return Error("Unsupported operation")},Ma.Fc=function(){return new W(null,1,5,X,[ge],null)},Ma.Tb=!0,Ma.wb="cljs.core/t_cljs$core13207",Ma.ic=function(a){return Ub(a,"cljs.core/t_cljs$core13207")});
function fe(){"undefined"===typeof Ma&&(Ma=function(a){this.mc=a;this.m=393216;this.C=0},Ma.prototype.U=function(a,b){return new Ma(b)},Ma.prototype.R=function(){return this.mc},Ma.prototype.Z=function(){return!1},Ma.prototype.next=function(){return Error("No such element")},Ma.prototype.remove=function(){return Error("Unsupported operation")},Ma.Fc=function(){return new W(null,1,5,X,[ge],null)},Ma.Tb=!0,Ma.wb="cljs.core/t_cljs$core13215",Ma.ic=function(a){return Ub(a,"cljs.core/t_cljs$core13215")});
return new Ma(he)}function ie(a,b){for(;;){if(null==I(b))return!0;var c=L(b);c=a.f?a.f(c):a.call(null,c);if(v(c)){c=a;var d=N(b);a=c;b=d}else return!1}}function je(a,b){for(;;)if(I(b)){var c=L(b);c=a.f?a.f(c):a.call(null,c);if(v(c))return c;c=a;var d=N(b);a=c;b=d}else return null}
function ke(a){return function(){function b(b,c){return $a(a.c?a.c(b,c):a.call(null,b,c))}function c(b){return $a(a.f?a.f(b):a.call(null,b))}function d(){return $a(a.v?a.v():a.call(null))}var e=null,f=function(){function b(a,b,d){var e=null;if(2<arguments.length){e=0;for(var f=Array(arguments.length-2);e<f.length;)f[e]=arguments[e+2],++e;e=new J(f,0,null)}return c.call(this,a,b,e)}function c(b,c,d){return $a(de(a,b,c,d))}b.F=2;b.D=function(a){var b=L(a);a=N(a);var d=L(a);a=zc(a);return c(b,d,a)};
b.w=c;return b}();e=function(a,e,l){switch(arguments.length){case 0:return d.call(this);case 1:return c.call(this,a);case 2:return b.call(this,a,e);default:var h=null;if(2<arguments.length){h=0;for(var k=Array(arguments.length-2);h<k.length;)k[h]=arguments[h+2],++h;h=new J(k,0,null)}return f.w(a,e,h)}throw Error("Invalid arity: "+(arguments.length-1));};e.F=2;e.D=f.D;e.v=d;e.f=c;e.c=b;e.w=f.w;return e}()}
@ -512,12 +512,12 @@ g.O=function(){null==this.A&&(this.A=vc(this.qb));return this.A};function mg(a){
function ng(){function a(){return Math.floor(16*Math.random()).toString(16)}var b=(8|3&Math.floor(16*Math.random())).toString(16);return mg([A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),"-",A.f(a()),A.f(a()),A.f(a()),A.f(a()),"-4",A.f(a()),A.f(a()),A.f(a()),"-",A.f(b),A.f(a()),A.f(a()),A.f(a()),"-",A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a()),A.f(a())].join(""))}
function og(a,b,c){var d=Error(a);this.message=a;this.data=b;this.Ib=c;this.name=d.name;this.description=d.description;this.number=d.number;this.fileName=d.fileName;this.lineNumber=d.lineNumber;this.columnNumber=d.columnNumber;this.stack=d.stack;return this}og.prototype.__proto__=Error.prototype;og.prototype.W=t;
og.prototype.M=function(a,b,c){Ub(b,"#error {:message ");Zf(this.message,b,c);v(this.data)&&(Ub(b,", :data "),Zf(this.data,b,c));v(this.Ib)&&(Ub(b,", :cause "),Zf(this.Ib,b,c));return Ub(b,"}")};og.prototype.toString=function(){return lc(this)};function pg(a,b){return new og(a,b,null)};var qg=new V("rum","react-component","rum/react-component",-1879897248),rg=new V(null,"did-mount","did-mount",918232960),sg=new V(null,"min","min",444991522),tg=new V(null,"will-unmount","will-unmount",-808051550),ug=new V(null,"email","email",1415816706),vg=new V("rum.examples.errors","error","rum.examples.errors/error",1819457764),Va=new V(null,"meta","meta",1499536964),wg=new V(null,"age","age",-604307804),xg=new V(null,"did-remount","did-remount",1362550500),yg=new V(null,"color","color",1011675173),
Wa=new V(null,"dup","dup",556298533),ge=new xc(null,"meta13208","meta13208",1259608997,null),zg=new V("rum","class","rum/class",-2030775258),Ag=new V(null,"init","init",-1875481434),Bg=new V("rum.examples.portals","*clicks","rum.examples.portals/*clicks",840774855),Cg=new V(null,"childContextTypes","childContextTypes",578717991),Dg=new V(null,"phone","phone",-763596057),Eg=new V(null,"content","content",15833224),Fg=new V(null,"msgData","msgData",345907944),Gg=new V(null,"did-catch","did-catch",2139522313),
Hg=new V(null,"child-context","child-context",-1375270295),Ig=new V(null,"margin-left","margin-left",2015598377),Jg=new V(null,"value","value",305978217),Kg=new V("rum.reactive","key","rum.reactive/key",-803425142),Lg=new V(null,"contextTypes","contextTypes",-2023853910),Mg=new V("rum","args","rum/args",1315791754),Ng=new V(null,"width","width",-384071477),fg=new V(null,"val","val",128701612),Og=new V(null,"cursor","cursor",1011937484),Pg=new V(null,"type","type",1174270348),Qg=new V(null,"timer-static",
"timer-static",1373464428),bg=new V(null,"fallback-impl","fallback-impl",-1501286995),Rg=new V(null,"before-render","before-render",71256781),Ta=new V(null,"flush-on-newline","flush-on-newline",-151457939),Sg=new V(null,"e","e",1381269198),Tg=new V(null,"className","className",-1983287057),Ee=new V(null,"style","style",-496642736),Ug=new V(null,"div","div",1057191632),Vg=new V(null,"did-update","did-update",-2143702256),Ua=new V(null,"readably","readably",1129599760),Tf=new V(null,"more-marker","more-marker",
-14717935),Wg=new V(null,"key-fn","key-fn",-636154479),Xg=new V(null,"g","g",1738089905),Yg=new V(null,"will-mount","will-mount",-434633071),Zg=new V(null,"c","c",-1763192079),$g=new V(null,"for","for",-1323786319),ah=new V("rum","component-stack","rum/component-stack",2037541138),bh=new V(null,"weight","weight",-1262796205),Xa=new V(null,"print-length","print-length",1931866356),ch=new V(null,"max","max",61366548),dh=new V(null,"id","id",-1388402092),eh=new V(null,"class","class",-2030961996),hh=
new V(null,"bmi","bmi",1421979636),ih=new V(null,"will-update","will-update",328062998),jh=new V(null,"on-mouse-move","on-mouse-move",-1386320874),kh=new V(null,"class-properties","class-properties",1351279702),lh=new V("rum","local","rum/local",-1497916586),mh=new V(null,"b","b",1482224470),nh=new V(null,"d","d",1972142424),oh=new V(null,"htmlFor","htmlFor",-1050291720),ph=new V("rum.examples.core","interval","rum.examples.core/interval",-891109255),qh=new V(null,"after-render","after-render",1997533433),
rh=new V(null,"static-properties","static-properties",-577838503),sh=new V(null,"tag","tag",-1290361223),th=new V(null,"input","input",556931961),uh=new V(null,"msgMethod","msgMethod",523741434),vh=new V(null,"wrap-render","wrap-render",1782000986),wh=new V(null,"on-change","on-change",-732046149),xh=new V("rum.reactive","refs","rum.reactive/refs",-814076325),ag=new V(null,"alt-impl","alt-impl",670969595),yh=new V(null,"backgroundColor","backgroundColor",1738438491),zh=new V(null,"should-update",
Wa=new V(null,"dup","dup",556298533),zg=new V("rum","class","rum/class",-2030775258),Ag=new V(null,"init","init",-1875481434),Bg=new V("rum.examples.portals","*clicks","rum.examples.portals/*clicks",840774855),Cg=new V(null,"childContextTypes","childContextTypes",578717991),Dg=new V(null,"phone","phone",-763596057),Eg=new V(null,"content","content",15833224),Fg=new V(null,"msgData","msgData",345907944),Gg=new V(null,"did-catch","did-catch",2139522313),Hg=new V(null,"child-context","child-context",
-1375270295),Ig=new V(null,"margin-left","margin-left",2015598377),Jg=new V(null,"value","value",305978217),Kg=new V("rum.reactive","key","rum.reactive/key",-803425142),Lg=new V(null,"contextTypes","contextTypes",-2023853910),Mg=new V("rum","args","rum/args",1315791754),Ng=new V(null,"width","width",-384071477),fg=new V(null,"val","val",128701612),Og=new V(null,"cursor","cursor",1011937484),Pg=new V(null,"type","type",1174270348),Qg=new V(null,"timer-static","timer-static",1373464428),bg=new V(null,
"fallback-impl","fallback-impl",-1501286995),Rg=new V(null,"before-render","before-render",71256781),Ta=new V(null,"flush-on-newline","flush-on-newline",-151457939),Sg=new V(null,"e","e",1381269198),Tg=new V(null,"className","className",-1983287057),Ee=new V(null,"style","style",-496642736),Ug=new V(null,"div","div",1057191632),Vg=new V(null,"did-update","did-update",-2143702256),Ua=new V(null,"readably","readably",1129599760),Tf=new V(null,"more-marker","more-marker",-14717935),Wg=new V(null,"key-fn",
"key-fn",-636154479),Xg=new V(null,"g","g",1738089905),Yg=new V(null,"will-mount","will-mount",-434633071),Zg=new V(null,"c","c",-1763192079),ge=new xc(null,"meta13216","meta13216",-501243119,null),$g=new V(null,"for","for",-1323786319),ah=new V("rum","component-stack","rum/component-stack",2037541138),bh=new V(null,"weight","weight",-1262796205),Xa=new V(null,"print-length","print-length",1931866356),ch=new V(null,"max","max",61366548),dh=new V(null,"id","id",-1388402092),eh=new V(null,"class","class",
-2030961996),hh=new V(null,"bmi","bmi",1421979636),ih=new V(null,"will-update","will-update",328062998),jh=new V(null,"on-mouse-move","on-mouse-move",-1386320874),kh=new V(null,"class-properties","class-properties",1351279702),lh=new V("rum","local","rum/local",-1497916586),mh=new V(null,"b","b",1482224470),nh=new V(null,"d","d",1972142424),oh=new V(null,"htmlFor","htmlFor",-1050291720),ph=new V("rum.examples.core","interval","rum.examples.core/interval",-891109255),qh=new V(null,"after-render","after-render",
1997533433),rh=new V(null,"static-properties","static-properties",-577838503),sh=new V(null,"tag","tag",-1290361223),th=new V(null,"input","input",556931961),uh=new V(null,"msgMethod","msgMethod",523741434),vh=new V(null,"wrap-render","wrap-render",1782000986),wh=new V(null,"on-change","on-change",-732046149),xh=new V("rum.reactive","refs","rum.reactive/refs",-814076325),ag=new V(null,"alt-impl","alt-impl",670969595),yh=new V(null,"backgroundColor","backgroundColor",1738438491),zh=new V(null,"should-update",
"should-update",-1292781795),Ah=new V(null,"a","a",-2123407586),Bh=new V(null,"height","height",1025178622);function Ch(a,b,c){this.ref=a;this.path=b;this.meta=c;this.m=2153938944;this.C=114690}g=Ch.prototype;g.equiv=function(a){return this.B(null,a)};g.M=function(a,b,c){Ub(b,"#object [rum.cursor.Cursor ");Zf(new u(null,1,[fg,this.va(null)],null),b,c);return Ub(b,"]")};g.R=function(){return this.meta};g.O=function(){return this[ba]||(this[ba]=++ca)};g.B=function(a,b){return this===b};g.bb=function(a,b){pe.G(this.ref,Be,this.path,b);return b};
g.Mb=function(a,b){var c=this;return c.bb(0,function(){var a=c.va(null);return b.f?b.f(a):b.call(null,a)}())};g.Nb=function(a,b,c){var d=this;return d.bb(0,function(){var a=d.va(null);return b.c?b.c(a,c):b.call(null,a,c)}())};g.Ob=function(a,b,c,d){var e=this;return e.bb(0,function(){var a=e.va(null);return b.j?b.j(a,c,d):b.call(null,a,c,d)}())};g.Pb=function(a,b,c,d,e){return this.bb(0,ee(b,this.va(null),c,d,e))};
g.Db=function(a,b,c){var d=this;gg(d.ref,kb(kb(M,b),this),function(a){return function(e,h,k,l){e=fb(H,k,d.path);l=fb(H,l,d.path);return Ac.c(e,l)?null:c.G?c.G(b,a,e,l):c.call(null,b,a,e,l)}}(this));return this};g.Eb=function(a,b){var c=kb(kb(M,b),this);Xb(this.ref,c);return this};g.va=function(){var a=F(this.ref);return fb(H,a,this.path)};var Dh;a:{var Eh=aa.navigator;if(Eh){var Fh=Eh.userAgent;if(Fh){Dh=Fh;break a}}Dh=""}function Gh(a){return-1!=Dh.indexOf(a)};function Hh(){return Gh("iPhone")&&!Gh("iPod")&&!Gh("iPad")};var Ih=Gh("Opera"),Jh=Gh("Trident")||Gh("MSIE"),Kh=Gh("Edge"),Lh=Gh("Gecko")&&!(-1!=Dh.toLowerCase().indexOf("webkit")&&!Gh("Edge"))&&!(Gh("Trident")||Gh("MSIE"))&&!Gh("Edge"),Mh=-1!=Dh.toLowerCase().indexOf("webkit")&&!Gh("Edge");Mh&&Gh("Mobile");Gh("Macintosh");Gh("Windows");Gh("Linux")||Gh("CrOS");var Nh=aa.navigator||null;Nh&&(Nh.appVersion||"").indexOf("X11");Gh("Android");Hh();Gh("iPad");Gh("iPod");Hh()||Gh("iPad")||Gh("iPod");function Oh(){var a=aa.document;return a?a.documentMode:void 0}var Ph;