Giter Club home page Giter Club logo

rum's Introduction

Rum is a client/server library for HTML UI. In ClojureScript, it works as React wrapper, in Clojure, it is a static HTML generator.

Table of Contents

Principles

Simple semantics: Rum is arguably smaller, simpler and more straightforward than React itself.

Decomplected: Rum is a library, not a framework. Use only the parts you need, throw away or replace what you don’t need, combine different approaches in a single app, or even combine Rum with other frameworks.

No enforced state model: Unlike Om, Reagent or Quiescent, Rum does not dictate where to keep your state. Instead, it works well with any storage: persistent data structures, atoms, DataScript, JavaScript objects, localStorage or any custom solution you can think of.

Extensible: the API is stable and explicitly defined, including the API between Rum internals. It lets you build custom behaviours that change components in significant ways.

Minimal codebase: You can become a Rum expert just by reading its source code (~900 lines).

Comparison to other frameworks

Rum:

  • does not dictate how to store your state,
  • has server-side rendering,
  • is much smaller.

Who’s using Rum?

Using Rum

Add to project.clj: [rum "0.12.11"]

API Docs & Articles

cljdoc badge

Defining a component

Use rum.core/defc (short for “define component”) to define a function that returns component markup:

(require [rum.core :as rum])

(rum/defc label [text]
  [:div {:class "label"} text])

Rum uses Hiccup-like syntax for defining markup:

[<tag-n-selector> <attrs>? <children>*]

<tag-n-selector> defines a tag, its id and classes:

  :span
  :span#id
  :span.class
  :span#id.class
  :span.class.class2

By default, if you omit the tag, div is assumed:

  :#id    === :div#id
  :.class === :div.class

<attrs> is an optional map of attributes:

  • Use kebab-case keywords for attributes (e.g. :allow-full-screen for allowFullScreen)
  • You can include :id and :class there as well
  • :class can be a string or a sequence of strings
  • :style, if needed, must be a map with kebab-case keywords
  • event handlers should be arity-one functions
[:input { :type      "text"
          :allow-full-screen true
          :id        "comment"
          :class     ["input_active" "input_error"]
          :style     { :background-color "#EEE"
                       :margin-left      42 }
          :on-change (fn [e]
                       (js/alert (.. e -target -value))) }]

<children> is a zero, one or many elements (strings or nested tags) with the same syntax:

  [:div {} "Text"]         ;; tag, attrs, nested text
  [:div {} [:span]]        ;; tag, attrs, nested tag
  [:div "Text"]            ;; omitted attrs
  [:div "A" [:em "B"] "C"] ;; 3 children, mix of text and tags

Children can include lists or sequences which will be flattened:

  [:div (list [:i "A"] [:b "B"])] === [:div [:i "A"] [:b "B"]]

By default all text nodes are escaped. To embed an unescaped string into a tag, add the :dangerouslySetInnerHTML attribute and omit children:

  [:div { :dangerouslySetInnerHTML {:__html "<span></span>"}}]

Rendering component

Given this code:

(require [rum.core :as rum])

(rum/defc repeat-label [n text]
  [:div (replicate n [:.label text])])

First, we need to create a component instance by calling its function:

(repeat-label 5 "abc")

Then we need to pass that instance to (rum.core/mount comp dom-node):

(rum/mount (repeat-label 5 "abc") js/document.body)

And we will get this result:

<body>
  <div>
    <div class="label">abc</div>
    <div class="label">abc</div>
    <div class="label">abc</div>
    <div class="label">abc</div>
    <div class="label">abc</div>
  </div>
</body>

Usually, mount is used just once in an app lifecycle to mount the top of your component tree to a page. After that, for a dynamic applications, you should either update your components or rely on them to update themselves.

Performance

Daiquiri, Rum's Hiccup compiler, pre-compiles certain Clojure forms that return Hiccup (for a list of these forms see compile-form implementations) into React calls. When the compiler is not able to pre-compile a form it defers this operation to the runtime. Runtime interpretation is slower, the suggestion is to use Clojure forms that are handled by compile-form, when it makes sense.

(rum/defc component []
  [:ul
    (for [n (range 10)]
      [:li n]) ;; `for` is a known form with a well defined syntax, thus Hiccup is pre-compiled
    (map (fn [n]
           [:li n]) ;; `map` is a generic higher-order function, can't reliably pre-compile, falling back to interpretation
      (range 10))])

To be informed about such code there's compiler flag that enables build warnings

(rum.core/set-warn-on-interpretation! true)

Updating components manually

The simplest way to update your app is to mount it again:

(rum/defc timer []
  [:div (.toISOString (js/Date.))])

(rum/mount (timer) js/document.body)

(js/setInterval
  #(rum/mount (timer) js/document.body)
  1000)

Reactive components

Rum offers mixins as a way to hook into a component’s lifecycle and extend its capabilities or change its behaviour.

One very common use-case is for a component to update when some reference changes. Rum has a rum.core/reactive mixin just for that:

(def count (atom 0))

(rum/defc counter < rum/reactive []
  [:div { :on-click (fn [_] (swap! count inc)) }
    "Clicks: " (rum/react count)])

(rum/mount (counter) js/document.body)

Two things are happening here:

  1. We’re adding the rum.core/reactive mixin to the component.
  2. We’re using rum.core/react instead of deref in the component body.

This will set up a watch on the count atom and will automatically call rum.core/request-render on the component each time the atom changes.

Component’s local state

Sometimes you need to keep track of some mutable data just inside a component and nowhere else. Rum provides the rum.core/local mixin. It’s a little trickier to use, so hold on:

  1. Each component in Rum has internal state associated with it, normally used by mixins and Rum internals.
  2. rum.core/local creates a mixin that will put an atom into the component’s state.
  3. rum.core/defcs is used instead of rum.core/defc. It allows you to get hold of the components’s state in the render function (it will be passed as a first argument).
  4. You can then extract that atom from the component’s state and deref/swap!/reset! it as usual.
  5. Any change to the atom will force the component to update.

In practice, it’s quite convenient to use:

(rum/defcs stateful < (rum/local 0 ::key)
  [state label]
  (let [local-atom (::key state)]
    [:div { :on-click (fn [_] (swap! local-atom inc)) }
      label ": " @local-atom]))

(rum/mount (stateful "Click count") js/document.body)

Optimizing with shouldComponentUpdate

If your component accepts only immutable data structures as arguments, it may be a good idea to add the rum.core/static mixin:

(rum/defc label < rum/static [n text]
  [:.label (replicate n text)])

rum.core/static will check if the arguments of a component’s constructor have changed (using Clojure’s -equiv semantic), and if they are the same, avoid re-rendering.

(rum/mount (label 1 "abc") body)
(rum/mount (label 1 "abc") body) ;; render won’t be called
(rum/mount (label 1 "xyz") body) ;; this will cause a re-render
(rum/mount (label 1 "xyz") body) ;; this won’t

Note that this is not enabled by default because a) comparisons can be expensive, and b) things will go wrong if you pass a mutable reference as an argument.

Writing your own mixin

Many applications have very specific requirements and custom optimization opportunities, so odds are you’ll be writing your own mixins.

Let’s see what a Rum component really is. Each Rum component has:

  • A render function
  • One or more mixins
  • An internal state map
  • A corresponding React component

For example, if we have this component defined:

(rum/defc input [label value]
  [:label label ": "
    [:input { :value value }]])

(input "Your name" "")

It will have the following state:

{ :rum/args ["Your name" ""]
  :rum/react-component <react-component> }

You can read the internal state by using the rum.core/defcs (short for “define component [and pass] state”) macro instead of rum.core/defc. It will pass state to the render function as the first argument:

(rum/defcs label [state label value]
  [:div "My args:" (pr-str (:rum/args state))])

(label "A" 3) ;; => <div>My args: ["A" 3]</div>

The internal state cannot be directly manipulated, except at certain stages of a component’s lifecycle. Mixins are functions that are invoked at these stages to give you and opportunity to modify the state and/or do side effects to the world.

The following mixin will record the component’s mount time:

(rum/defcs time-label
  < { :will-mount (fn [state]
                    (assoc state ::time (js/Date.))) }
  [state label]
  [:div label ": " (str (::time state))])

As you can see, :will-mount is a function from state to state. It gives you a chance to populate, clean or modify state map the moment before the component has been mounted.

Another useful thing you can do in a mixin is to decide when to update a component. If you can get ahold of React component (notice that that’s different from Rum component, unfortunately; sorry), you can call rum.core/request-render to schedule this component’s update. To get React component, just look up :rum/react-component key in a state.

This mixin will update a component each second:

(def periodic-update-mixin
  { :did-mount    (fn [state]
                    (let [comp      (:rum/react-component state)
                          callback #(rum/request-render comp)
                          interval  (js/setInterval callback 1000)]
                       (assoc state ::interval interval)))
    :will-unmount (fn [state]
                    (js/clearInterval (::interval state))
                    (dissoc state ::interval)) })

(rum/defc timer < periodic-update-mixin []
  [:div (.toISOString (js/Date.))])

(rum/mount (timer) js/document.body)

Here’s a full list of callbacks you can define in a mixin:

{ :init                 ;; state, props     ⇒ state
  :will-mount           ;; state            ⇒ state
  :before-render        ;; state            ⇒ state
  :wrap-render          ;; render-fn        ⇒ render-fn
  :render               ;; state            ⇒ [pseudo-dom state]
  :did-catch            ;; state, err, info ⇒ state
  :did-mount            ;; state            ⇒ state
  :after-render         ;; state            ⇒ state
  :will-remount         ;; old-state, state ⇒ state
  :should-update        ;; old-state, state ⇒ boolean
  :will-update          ;; state            ⇒ state
  :did-update           ;; state            ⇒ state
  :will-unmount }       ;; state            ⇒ state

Each component can have any number of mixins:

(rum/defcs component
  < rum/static
    rum/reactive
    (rum/local 0 ::count)
    (rum/local "" ::text)
  [state label]
  (let [count-atom (::count state)
        text-atom  (::text state)]
    [:div])

One gotcha: don’t forget to return state from the mixin functions. If you’re using them for side-effects only, just return an unmodified state.

Working with atoms

Since Rum relies a lot at components being able to efficiently update themselves in reaction to events, it includes two facilities to build architectures around Atoms and watchers.

Cursors

If you have a complex state and need a component to interact with only a part of it, create a cursor using (rum.core/cursor-in ref path). Given atom with deep nested value and path inside it, cursor-in will create an atom-like structure that can be used separately from main atom, but will sync changes both ways:

(def state (atom { :color "#cc3333"
                   :user { :name "Ivan" } }))

(def user-name (rum/cursor-in state [:user :name]))

@user-name ;; => "Ivan"

(reset! user-name "Oleg") ;; => "Oleg"

@state ;; => { :color "#cc3333"
       ;;      :user  { :name "Oleg" } }

Cursors implement IAtom and IWatchable and interface-wise are drop-in replacement for regular atoms. They work well with rum/reactive and rum/react too.

Derived atoms

Use derived atoms 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

Derived atoms are like cursors, but can “depend on” multiple references and won’t sync changes back to the source if you try to update derived atom (don’t).

Interop with React

Native React component

You can access the raw React component by reading the state’s :rum/react-component attribute:

{ :did-mount (fn [state]
               (let [comp     (:rum/react-component state)
                     dom-node (js/ReactDOM.findDOMNode comp)]
                 (set! (.-width (.-style dom-node)) "100px"))
               state) }

React keys and refs

There’re three ways to specify React keys:

  1. If you need a key on Sablono tag, put it into attributes: [:div { :key "x" }]
  2. If you need a key on Rum component, use with-key:
(rum/defc my-component [str]
  ...)

(rum/with-key (my-component "args") "x")
  1. or, you can specify :key-fn in a mixin to calculate key based on args at component creation time:
(rum/defc my-component
  < { :key-fn (fn [x y z]
                (str x "-" y "-" z)) }
  [x y z]
  ...)

(my-component 1 2 3) ;; => key == "1-2-3"

:key-fn must accept same arguments your render function does.

Refs work the same way as options 1 and 2 for keys work:

  1. [:div { :ref "x" }]
  2. (rum/with-ref (my-component) "x")

Accessing DOM

[:div {:ref (fn [node] ...)}]

;; or

(let [ref (rum/create-ref)]
  [:input
    {:ref ref
     :on-change #(.log js/console (rum/deref ref))}])

⚠️ The helpers below are deprecated since usage of string refs has been deprecated in React itself. Instead use the API described above.

There’re couple of helpers that will, given state map, find stuff in it for you:

(rum/dom-node state)     ;; => top-level DOM node
(rum/ref      state "x") ;; => ref-ed React component
(rum/ref-node state "x") ;; => top-level DOM node of ref-ed React component

Custom class properties

To define arbitrary properties and methods on a component class, specify a :class-properties map in a mixin:

(rum/defc comp
  < { :class-properties { ... } }
  [:div]))

To define static properties on a component class, specify a :static-properties map in a mixin:

(rum/defc comp
  < { :static-properties { ... } }
  [:div]))

React context

New API
(rum/defcontext *context*)

(rum/defc context-consumer []
 (rum/with-context [value *context*]
   value)) ;; "hello"

(rum/defc context-provider []
  (rum/bind-context [*context* "hello"]
    (context-consumer))
Legacy API

⚠️ This API is deprecated in React and will be removed in future versions of Rum

To define child context

  1. Add dependency [cljsjs/prop-types "15.5.10-1"]
  2. (require [cljsjs.prop-types])
  3. Specify a :child-context function taking state and returning context map in a mixin:
(rum/defc theme
  < { :child-context
      (fn [state]
        (let [[color] (:rum/args state)]
          { :color color }))
      :static-properties
      { :childContextTypes {:color js/PropTypes.string} } }
  [color child]
  child)

React Hooks

There are Rum wrappers for the various React hooks. See doc strings for examples, and the React hooks reference for more details.

⚠️ Hooks can be used only in defc components with optional rum/static mixin. Using any other mixin or form of declaring a component will generate class-based React components that are not compatible with hooks. You should use either hooks or mixins in one component, two can't work together.

;; Takes initial value or value returning fn and returns a tuple of [value set-value!],
;; where `value` is current state value and `set-value!` is a function that schedules re-render.
(let [[x set-x!] (rum/use-state 0)]
  (set-x! (inc x)))

;; Takes reducing function and initial state value.
;; Returns a tuple of [value dispatch!], where `value` is current state value and `dispatch` is a function that schedules re-render.
(rum/use-reducer reducer-fn initial-value)

;; Takes setup-fn that executes either on the first render or after every update.
;; The function may return cleanup-fn to cleanup the effect, either before unmount or before every next update.
;; Calling behavior is controlled by deps argument.
(rum/use-effect!
  (fn []
    (.addEventListener js/document "keydown" js/console.log)
    #(.removeEventListener js/document "keydown" js/console.log))
  [])

;; Takes callback function and returns memoized variant, memoization is done based on provided deps collection.
(rum/defc component [x]
  (let [on-change (rum/use-callback #(js/console.log % x) [x])]
    [input-field {:on-change on-change}]))

;; Takes a function, memoizes it based on provided deps collection and executes immediately returning a result.
(let [x (rum/use-memo #(expensive-computation v) [v])])

;; Takes a value and puts it into a mutable container which is persisted for the full lifetime of the component.
(rum/defc component []
  (let [ref (rum/use-ref nil)]
    (rum/use-effect!
      #(.log js/console (rum/deref ref)))
    [:input {:ref ref}]))

React Fragment

Using :<> as the tag in a markup vector creates a React Fragment, allowing you to render multiple components without a wrapping element.

[:<>
  [:span]
  [:div]
  [:span]]

;; <span></span><div></div><span></span>

Server-side rendering

When used from cljs Rum delegates serialization to ReactDOM library. If used from clj/cljc, Rum works as a traditional template engine à la Hiccup:

  1. Rum’s project.clj dependency becomes [rum "0.12.11" :exclusions [cljsjs/react cljsjs/react-dom]
  2. Import rum.core as usual.
  3. Define components using rum/defc or other macros as usual.
  4. Instead of mounting, call rum/render-html to render into a string.
  5. Generate the HTML page using that string.
  6. On the client side, mount (but using rum/hydrate) the same component over the node where you rendered your server-side component.
(require '[rum.core :as rum])

(rum/defc my-comp [s]
  [:div s])

;; on a server
(rum/render-html (my-comp "hello"))
;; => "<div data-reactroot=\"\">hello</div>"

;; on a client
(rum/hydrate (my-comp "hello") js/document.body)

Use rum/render-static-markup if you’re not planning to connect your page with React later:

(rum/render-static-markup (my-comp "hello")) ;; => <div>hello</div>

Rum server-side rendering does not use React or Sablono, it runs completely in JVM, without involving JavaScript at any stage.

As of [rum "0.8.3"] and [hiccup "1.0.5"], Rum is ~3× times faster than Hiccup.

Server-side components do not have full lifecycle support, but :init and :will-mount from mixins would be called at the component’s construction time.

Support

Talks

App templates

Libraries

  • Reforms, Bootstrap 3 forms
  • rum-mdl, Material design lite components
  • derivatives, creates chains of derived values from an atom
  • citrus, state coordination framework (previously known as scrum)
  • Antizer Ant Design component library
  • data-frisk-rum, display current value of data

Examples

Acknowledgements

Rum was build on inspiration from Quiescent, Om and Reagent.

All heavy lifting done by React and ClojureScript.

License

Copyright © 2014 Nikita Prokopov, 2020 Roman Liutikov

Licensed under Eclipse Public License (see LICENSE).

rum's People

Contributors

ajchemist avatar avasenin avatar azzurite avatar bfontaine avatar clifford avatar djebbz avatar dottedmag avatar dwwoelfel avatar fierycod avatar green-coder avatar jetmind avatar kennyjwilli avatar leontalbot avatar lynaghk avatar martinklepsch avatar mpenttila avatar nickpell avatar niwinz avatar peeb avatar piranha avatar prestancedesign avatar priornix avatar robert-stuttaford avatar roman01la avatar scgilardi avatar stuarth avatar tiensonqin avatar tomasd avatar tonsky avatar zelark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

rum's Issues

is it possible to get a rum component by data-reactid attribute value?

I'm working on a mixin that inject styles into component and sub-component
this mixin takes a function that take the component state and return a style hash-map and possibly another hash-map of type sub-element-selector -> style-function and so on
for the moment I use dommy for the sub-element-selectors and get raw HTMLelements , i would like to be able to get the value of the rum element that is mounted on in order to feed the corresponding style-fn. I really hope that it is understandable

Is there a way to get a list of all sub rum components of a rum component?

Better feedback when mixing argvec/mixin order

It has happened to me a few times now that I got stuck because I put the mixin in the wrong place.

(defc x
  [a b]
  < rum/static
  [:div "bla"])

I think this is something that could be improved to give better feedback.
While throwing an exception isn't guaranteed to be correct the probability of false positives seems low (i.e. component bodies starting with a plain <).

Would you be interested in a patch that throws for the above snippet? Do you have alternative suggestions that might be less absolute?

Maybe throwing an exception is a good first step and can be revisited once there is some user feedback.

[question] Redundant wrapping (React) components in self-referencing (Rum) components

In case multi-arity component's fns just call each other with additional arguments, each arity of such self-referencing component still creates its own React component. For instance, this is React's components hierarchy for the tree example component (notice two tree's, outer one created by (tree [:a [:b [:c :d [:e] :g]]]), and inner one - by (tree [:a [:b [:c :d [:e] :g]]] 0)):

tree component hierarchy

Such behaviour is quite logical when each arity produces slightly different components (or completely different ones), but when they simply call next arity with additional default arguments (like tree example) wrapping components seem redundant, and can cause some problems (for example, if custom did-mount is used, it would be called for each wrapping component).

So, is there any chance that in future Rum would support addition of mixins to the specific arities of multi-arity component and/or would remove redundant wrappers from produced component hierarchy of multi-arity component?

Indentation support for Emacs

Problem

In emacs, the mixin macro does not get indented properly. You have to either write your components like this:

(rum/defc text [] < rum/reactive (rum/local "" ::text) ...)

Or, like this:

(rum/defc text [] < rum/reactive
  (rum/local "" ::text)
...)

This unfortunately is not pleasing to the eye at all, and can get confusing very quickly as you add more mixins to your component.

Proposed solution

The Emacs plugin, cider, allows you to specify how it should indent a function or macro. You can read about that here. Adding this to rum would be incredibly helpful!

How to create materialised views from a single state atom (à la Reagent's track)?

Let's say I have the following state:

(def model 
  (atom
  {:people/by-id {1 {:id 1 :name "Bob"} 2 {:id 2 :name "Jane"}}
   :departments/by-id {0 {:name "Software" :people #{} :id 0}
                       1 {:name "Marketing" :people #{} :id 1}
                       2 {:name "HR" :people #{[:people/by-id 1] [:people/by-id 2]} :id 2}}}))

Using Reagent's track I could get a materialised view into the departments, meaning that they'd have the actual employee data and not just the idents, by first defining a function something like this:

(defn materialised-departments [atom]
  (map
    (fn [dept]
      (assoc dept :people
                  (set (map
                         (fn [emp-q]
                           (get-in @atom emp-q))
                         (:people dept)))))
    (vals (get-in @atom [:departments/by-id]))))

Then I would use the above function with r/track, to create a materialised view of the departments. My components will handle it just like a normal ratom, and react to its changes.

(let [departments (r/track model/materialised-departments atom)])

Rum (like Reagent) has cursor, but I don't know if it can be used to accomplish something like this. Honestly, I'm not sure if this issue is a support request / question, or a valid feature request. I'd be happy to hear your thoughts on this!

rum.core/derived-atom without docs

rum.derived-atom/derived-atom has a nice docstring, but it doesn't transfer over to rum.core/derived-atom, which appears to be undocumented. A fix could be to move the docstring to the def in rum.core.

Sharing more examples

Based on my work of porting my app to Rum, I would like to extract some of the mix-ins I have created into stand alone examples people can refer to. I would like your input on how best to share this work with the community.

Some possibilities:

  1. series of Gists that you can link to from the README if you choose
  2. pages in the Wiki (maybe a Rum Cookbook, like they are doing for Reagent)
  3. add to the examples in the project repo

Do you have a preference?

datascript -> rum/render -> should-update

I'm playing around with rum, using the idea of https://github.com/tonsky/datascript-chat - I'm omtting the async/chan part for this example. The application is rather heavily query-based, and I want to implement a :should-update mixin, which will update the components only if the queries are changed (sounds pretty obvious).
I have something like:

(def root (rum/mount (body) (.-body js/document)))

(defonce init (fn [] 
   (ds/listen! conn (fn [tx-report]
       (rum/request-render root)))))

(init)

Any updates in the database trigger a rum/request-render. Now, the 'should-update' mixin looks like:

(rum/defc comp < {:should-update (fn [o n] ... )} [args]  
    (let [q (ds/q ...)]
        ... ))

However, the 'old' state and the 'new' state are not the old and new state of the transaction (or the component as such), but rather some intermediate states of the component (so it seems to me). The old value is not the 'previous query result' or 'previous state which led to that query result'. What I'd like is to compare the previous result of the query with the new result of the query.

What's the most logic/best way to do this using rum?

Using build-class

Following the 'how it all fits together' section in the docs, I'm trying to create and mount a simple React element without using defc.

I've created a fresh project with the following code:

(def a
    (rum/build-class
        {:render
         (fn [state]
             [(sablono.core/html [:div "a"]) state])}
        "a-component"))

(rum/mount (rum/element a {} nil) (.-body js/document))

Should this work? it gives me a Uncaught TypeError: Cannot read property 'call' of null ...

stacktrace:

rum.build_class.React.createClass.render    @   rum.cljs?rel=1441294910065:96
ReactCompositeComponentMixin._renderValidatedComponentWithoutOwnerOrContext @   react.inc.js:6975
ReactCompositeComponentMixin._renderValidatedComponent  @   react.inc.js:7002
ReactPerf.measure.wrapper   @   react.inc.js:13427
ReactCompositeComponentMixin.mountComponent @   react.inc.js:6423
ReactPerf.measure.wrapper   @   react.inc.js:13427

relevant code point:

      :render
      (fn []
        (this-as this
          (let [state (state this)
                [dom next-state] (wrapped-render @state)] ; <<<<<
            (vreset! state next-state)
            dom)))

I realize that part of the docs is already incorrect because build-class requires two arguments, and the label-mixin has a nested map. I'd submit a pull request about those issues but I'd like to get an example working first.

Using React's Perf tools to profile rum apps

It took me a while to figure out how to get React's Perf tools working within ClojureScript, and I think it'd be helpful for others to have a section in the Rum README.
I've enclosed a draft below.

I'm not sure the best way to configure Leiningen / cljsbuild / figwheel to keep the Perf tools only in development builds (or if they'll get advanced-optimizationed away) --- I bet it could be done with Leiningen's project.clj :profiles key, but there may be a better way.
I'd love if someone could chime in on that.

## Profiling

To use [React.js Perf tools](https://facebook.github.io/react/docs/perf.html) from Rum, specify the `react-with-addons` dependency in your `project.clj`, making sure to exclude any transitive react dependencies:

```clj
[rum "0.9.0" :exclusions [cljsjs/react cljsjs/react-dom]]
[cljsjs/react-dom "15.1.0-0" :exclusions [cljsjs/react]]
[cljsjs/react-dom-server "15.1.0-0" :exclusions [cljsjs/react]]
[cljsjs/react-with-addons "15.1.0-0"]
```

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.

Problem with sibling children's keys

Today i've updated rum to 0.3.0 version and this warning appears:

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of pic-likes. See https://fb.me/react-warning-keys for more information.

My pic-likes component looks like:

(rum/defc pic-likes < rum/static
  [{:keys [likes] :as image} user]
  (let [likes-count (count likes)
        liked (contains? likes (:id user))]
    [:a {:class-name "pic-likes"
         :href "#like"
         :on-click #(do
                      (.preventDefault %)
                      (toggle-pic-like image user))}
     (pic-likes-counter likes-count)
     (icon {:class-name "pic-likes-icon"
            :icon (if liked "favorite" "favorite-outline")})]))

If I wrap both pic-counter and icon components with with-key function and give them different keys - problem disappears. But it's kind of strange, you can see I don't iterate over these blocks. They are just siblings.

ClojureScript 0.0-2727 and sablono 0.3.x

The latest version of sablono now depends on [cljsjs/react] which breaks rum in avanced compilation mode whilst it still depends on [com.facebook/react] (because there are two competing externs files).

Would you be interested in a pull request that updates the codebase to depend on [sablono "0.3.1"] and [cljsjs/react "0.12.2-5"] and the new foreign libraries features of ClojureScript?

This has the addiontal benefit that you no longer need both index.html and web/dev.html since all optimization levels now work with the same template.

Reasons for Rum

I really like rum. Thanks, @tonsky! You have good taste.

Consider adding your reasons for making Rum to the README, to help others decide when and why to choose use Rum over Om, Reagent or Quiescent.

Order of arguments to defc

defc is different from almost all of the other def-something forms seen in the Clojure world in that its first argument is not the name of the thing being defined. Would you consider changing the semantics of it to be (defc name mixin(s) args & body) instead? I'm guessing your intention was to avoid having two vectors following each other, which might look confusing to a lot of people, but the way it works right now breaks syntax highlighting with most editors.

This is obviously a totally breaking change for a rather petty reason, but I figured that since the project is still so young this might not be such a big deal.

Form controls generate React warnings

Using any HTML form controls, e.g. [:input] causes React to emit the following warnings:

Warning: Something is calling a React component directly. Use a factory or JSX instead. See: http://fb.me/react-legacyfactory

Warning: transferPropsTo is deprecated. See http://fb.me/react-transferpropsto for more information.

I spent some time tracking this down to make sure it wasn't something I was doing wrong, and I found the problem in sablono. They create a new React component to wrap each form control, and they are using createClass instead of createElement. This has been fixed in master, although I could not get my app to build using the 3.0.0-snapshot version published to clojars.

I created this issue mainly as a heads up for anyone else who runs into it, so they know that it's expected behavior and nothing is wrong. Also, maybe this can serve as a reminder to update the sablono dependency when they release a new version.

DataScript integration?

rum seems very interesting! I'd love to have components that get data from DataScript and automatically re-render on change. Do you think that is easy to do with rum?

Argument destructuring is not working properly

Code like this:

(rum/defc Label [{:keys [id text] :as item}]
  (js/console.log (pr-str item) (pr-str text))
  [:div {:key id} (str text " ") [:span.badge id]])

outputs this to console:

 {:keys [2 "Two"], :as {:id 2, :text "Two"}} nil

Question about state in mixins

In some cases rum\react is loosing non-default values in state between :did-mount and :will-unmount hooks in mixin. It doesn't happen if we manually transfer our state in :transfer-state hook.

The following example describes the issue https://gist.github.com/narma/46a990def0e1c6f279a2
In this code errors appear if we use dynamic count of children or we change parent elements.

Errors don't happen if we use constant count of children and don't touch parent elements because :will-unmount doesn't fire at all.

Is this expected and normal behaviour or is this a bug?

What is the idiomatic way of managing state?

In reagent you can return the render function instead of directly returning the component description.1
So you can do this, for example:

(defn timer-component []
  (let [seconds-elapsed (reagent/atom 0)]     ;; setup, and local state
    (fn []        ;; inner, render function is returned
      (js/setTimeout #(swap! seconds-elapsed inc) 1000)
      [:div "Seconds Elapsed: " @seconds-elapsed])))

I'm wondering what is the corresponding solution in rum.

way to add custom attributes to react component?

Currently react has attributes whitelist, to be able to use custom attributes you can apply this workaround
(from http://stackoverflow.com/questions/31273093/how-to-add-custom-html-attributes-in-jsx):

Custom attributes are currently not supported. See this open issue for more info: facebook/react#140
As a workaround, you can do something like this in componentDidMount:
componentDidMount: function() {
this.refs.test.getDOMNode().setAttribute('custom-attribute', 'some value');
}
See https://jsfiddle.net/peterjmag/kysymow0/ for a working example. (Inspired by syranide's suggestion in this comment.)

How i can use this workaround with rum? i need to add webkitdirectory to file upload.

Thanks!

derived-atom: remove watches on source atoms

When using derived-atom a watch is added to all source atoms but there's no way to remove these watches potentially causing stale watches when the derived atom is no longer needed.

I have a very rough implementation that extends the derived atom implementation by a IDisposable protocol which provides a method that will remove all relevant watches on source atoms:
https://github.com/martinklepsch/derivatives/blob/disposable/src/org/martinklepsch/derived.cljc

Watching a derived atom is broken in this implementation because it's not an atom but that's easily fixable.

Now the question, would you be interested in extending Rum's derived atom to cover this aspect?

Provide a way to exclude cljsjs/react

I want to use my own React build. I added

:dependencies [[rum "0.2.6"]]
:exclusions [cljsjs/react]

to my project.clj but it can't compile Rum now giving this error:

clojure.lang.ExceptionInfo: No such namespace: cljsjs.react at line 1 file:/Users/petrus/.m2/repository/rum/rum/0.2.6/rum-0.2.6.jar!/rum.cljs {:tag :cljs/analysis-error, :file "file:/Users/petrus/.m2/repository/rum/rum/0.2.6/rum-0.2.6.jar!/rum.cljs", :line 1, :column 1}

Is there another way for doing this?

(We were talking about this in chat, but probably here is a better place for discussion).

Template snippet rendered differently on client and server

I have a (rum) hiccup-ish snippet in a template, that is rendered differently by the server side code than it is in the client side code, resulting in a React warning.

Sample code
;; component in cljc
(rum/defc app []
  [:input#topics {:type "text"}])

;; server in clj
(rum/render-html (components/app))

;; client in cljs
(rum/mount (components/app) node)
Resulting warning

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:

 (client) <input type="text" id="topi
 (server) <input id="topics" type="te
Some thoughts

Potential fix might be achieved by using something like https://github.com/amalloy/ordered on both clj and cljs sides, or sorted-map.

React 0.13 warning about mutated props

I could just be doing something wrong but Rum seems to be running afoul of the change in React 0.13 to immutable props. See here and here.

(defonce app-state (atom {:text "Hello World!"}))

(rum/defc app < rum/cursored rum/cursored-watch [app-state]
          [:h1 (:text @app-state)])

(defn main []
  (rum/mount
    (app app-state)
    (. js/document (getElementById "app"))))
Warning: Don't set .props.:rum/state of the React component <app />. Instead, specify the correct value when initially creating the element or use React.cloneElement to make a new element with updated props.

noscript is not used in react 15

Right now Rum returns [:noscript] on server when there is no DOM generated.

What it needs to return is a comment like <!--react-empty: %d-->, where %d is a data-reactid (number from the same list of numbers, also taken by sequence).

I'm not exactly sure what's the best way to implement that...

[question] How to pass state across tree

I have one single atom for state.
Some components may want to watch some parts of this atom.
But their parents pure components without data.

For example: page header contain user control. Header doesnt have any data from state, but user control have. In react I pass this state by context. And wrap user control in container that watch data in state and pass as props to user control. I can do it with rum, but I want to know is this best way?

Also my question not about only state atom, but about all things components may want deep in tree.
In my case is state and dispatcher (chan).

Using external react components

This might be be just something missing from documentation, but I'm having a small issue getting external components to work. I'm using that like this:

[(.-Route js/ReactRouter) {:name "home"} ]

where window.ReactRouter.Route is the component I want to use. It throws an error from this function.

(defn normalize-element
  "Ensure an element vector is of the form [tag-name attrs content]."
  [[tag & content]]
  (when (not (or (keyword? tag) (symbol? tag) (string? tag)))
    (throw (ex-info (str tag " is not a valid element name.") {:tag tag :content content})))
  (let [[tag id class] (match-tag tag)
        tag-attrs (compact-map {:id id :class class})
        map-attrs (first content)]
    (if (map? map-attrs)
      [tag (merge-with-class tag-attrs map-attrs) (next content)]
      [tag tag-attrs content])))

Any pointers on how to get non rum components to work?

Escape attributes in server-side

By @dwwoelfel:

I noticed that attributes aren't escaped when Rum builds html with its server-side rendering.

For example,

[:div {:className "\"><script>alert('hi')</script>"}]

is perfectly safe on the cljs side, but will alert if it's rendered on the server.

Render on atom change outside of event handler, like Reagent

Hello,

In the Readme file, it says:

On higher level, Rum comes with already-built types of components which emulate behavior found in Om, Reagent and Quiescent"

It makes me think that Rum can already work exactly like Reagent, but I think that is not the case because one feature that Reagent can do that Rum cannot do out of the box is the following, as referenced from https://reagent-project.github.io/news/reagent-is-async.html:

... proper batching of renderings even when changes to atoms are done outside of event handlers (which is great for e.g core.async users).

So is there a plan to match at least that feature of Reagent?

Example piece of code below shows an input that can only accept numbers. Typing in alphabet keys will not render the alphabets at all in the input in Reagent, but does render for a split second with the Rum code below.

(def text (atom ""))
(def my-chan (chan))

(rum/defc num-input < rum/reactive []
  [:input {:value (rum/react text) :on-change
    (fn [e] (put! my-chan (string/replace (.-value (.-target e)) #"[a-zA-Z]*" "")))}])

(go
  (while true
    (reset! text (string/replace (<! my-chan) #"[a-zA-Z]*" ""))))

(rum/mount (num-input) js/document.body)

@ajchemist has already suggested a solution by using (rum/request-render component) but must be inside the event handler. However does that match the behaviour of Reagent?

Kind regards,
Robin

React keys warning when component body within let

Using

[rum "0.2.7" :exclusions [cljsjs/react]]
[cljsjs/react-with-addons "0.12.2-7"]

Rum causes a React keys warning when a component render body is within a let.
The minimal test case below:

(defn raw-sablono
  []
  (sablono.core/html
   [:div
    [:span "A"]
    [:span "B"]]))


(defn let-sablono
  []
  (sablono.core/html
   (let [x 1]
     [:div
      [:span "A"]
      [:span "B"]])))


(rum/defc raw-rum
  []
  [:div
   [:span "A"]
   [:span "B"]])


(rum/defc let-rum
  []
  (let [x 1]
    [:div
     [:span "A"]
     [:span "B"]]))


(rum/defc app
  []
  [:div
   (raw-sablono)
   (let-sablono)
   (raw-rum)
   (let-rum)])


(.render js/React (app) (.getElementById js/document "container"))

Causes the following warnings in the JavaScript console:

Each child in an array should have a unique "key" prop. Check the render method of app. See http://fb.me/react-warning-keys for more information.
Each child in an array should have a unique "key" prop. Check the render method of let-rum. See http://fb.me/react-warning-keys for more information.

I'm confused by both warnings since none of this code is using sequences --- everything should compile into direct React calls without intermediate arrays.

I've tried debugging by expanding rum's output in a Clojure REPL:

(macroexpand
   '(rum/defc app
      []
      [:div
       [:span]
       [:span]]))

but it blows up:

1. Unhandled java.lang.IllegalArgumentException
   Syntax error at ([] [:div [:span] [:span]])

                       rum.clj:   25  rum/parse-defc
                       rum.clj:   31  rum/-defc
                       rum.clj:   54  rum/defc

Sharing state from root to all nested components

In Om, mounting the component root with om/root also allows for supplying shared state which can then be accessed by any component under the root component via om/get-shared. This allows for global data and services to be set without resorting to global state since all state is still local to the mounted component tree. This allows, for example, multiple mounts of the same app on the same page with "global" data and services independent from each other.

Would it be possible to include something similar to rum? Or are there any suggested patterns to get the same result?

circular dependency between rum.core and rum.core

I am not sure I should file this here. But the dependency in core.cljs on core.clj for macros seem to confuse CIDER refactor tool.

{:reason :mranderson046.toolsnamespace.v0v3v0-alpha1.clojure.tools.namespace.dependency/circular-dependency, :node rum.core, :dependency rum.core}

Argument Restructuring / Variadic Arguments

(rum/defc input1 < rum/reactive [ref & [attrs]]
  [:input (merge {:type "text"
                  :value @ref
                  :on-change #(reset! ref (.. % -target -value))}
                 attrs)])

 (rum/defc input2 < rum/reactive
  ([ref] (input ref nil)
  ([ref attrs]
   [:input (merge {:type "text"
                   :value @ref
                   :on-change #(reset! ref (.. % -target -value))}
                  attrs)]))

I think the two defc calls above don't work. input1 causes:

WARNING: Use of undeclared Var fcc.app/& at line 12 /Users/martin/.boot/tmp/Users/martin/projects/fun-club/ta/-p9hf6h/fcc/app.cljs

input2 causes

WARNING: Use of undeclared Var fcc.app/attrs at line 16 /Users/martin/.boot/tmp/Users/martin/projects/fun-club/30i/-p9hf6h/fcc/app.cljs

Checking for list in fn-body?

In rum.clj, there is

(defn- fn-body? [form]
  (and (list? form)
       (vector? (first form))))

Why do we need to check if form is a list here? The reason I ask is that I'm writing a macro that generates a defcs form. I can't do this because in clojure form is an ArraySeq. form appears to only be a list when defcs is expanded directly from cljs.

Patterns for efficient rendering of components that take callbacks

I'm using Rum to build reusable components.
These components typically don't have local state --- they're "controlled components" that take a value and on-change callback as arguments:

(rum/defc *color-picker < rum/static
  [color on-change]
  [:input {:type "color" :value color :on-change #(on-change (.-value (.-target %)))}])

Their parents use this callback to propagate new values across the app:

(rum/defc *parent < rum/static
  [parent-id color]
  [:div
   [:h1 {:style {:background-color color}} parent-id]
   (*color-picker color #(update-color-elsewhere-in-app! parent-id %))])

Semantically everything works great, but I'm having trouble with performance.
Using rum/static doesn't help because the on-change callbacks are anonymous functions --- on each render of the parent's body, a new on-change is created that is not equal to the previous on-change callback.

Are there any recommended patterns for handling this situation?
The only solution I can think of is to pass the functions in as vars rather than closures.
E.g., the above example would become:

(rum/defc *color-picker < rum/static
  [color on-change-args]
  [:input {:type "color" :value color :on-change #(apply (first on-change-args) (rest on-change-args) (.-value (.-target %))}])

(rum/defc *parent < rum/static
  [parent-id color]
  [:div
   [:h1 {:style {:background-color color}} parent-id]
   (*color-picker color [update-color-elsewhere-in-app! parent-id]))])

but this doesn't strike me as a great solution.

ReactDOM.unstable_batchedUpdates breaks ReactNative

Hi,

Rum works really well in re-natal w/ react-native. However, recent (unreleased) change "Do all re-renders on requestAnimationFrame in a single batch" breaks it. The same is available in react native though.

https://github.com/facebook/react-native/blob/803cb61346bcdf85d68df5cdfc1a392f78e6cac3/Libraries/react-native/react-native.js#L156

I did a workaround like this. It works. Maybe there could be a better way rum can support.

(set! js/window.ReactDOM (js/require "react-native"))

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.