Skip to content
Browse files
first stage of server-side compilation
  • Loading branch information
piranha committed Jan 30, 2016
1 parent f96e298 commit 5add8dfb4d1e077b0b63a9b2eb76d77ec593817d
Showing with 283 additions and 29 deletions.
  1. +178 −0 examples/rum/server_examples.clj
  2. +5 −4 project.clj
  3. +42 −9 src/rum/core.clj
  4. +2 −16 src/rum/core.cljs
  5. +39 −0 src/rum/server.clj
  6. +17 −0 src/rum/utils.cljc
@@ -0,0 +1,178 @@
(ns rum.server-examples
[clojure.string :as str]
[rum.core :as rum]))

(defn now []
(.getTime (java.util.Date.)))

(def iso-format (java.text.SimpleDateFormat. "yyyy-MM-dd'T'HH:mm'Z'"))
(defn ts->str [ts]
(let [date (java.util.Date. ts)
str (.format iso-format date)]
(subs str 11 (dec (count str)))))

(def clock (atom (now)))
(def color (atom "#FA8D97"))
(def speed (atom 167))
(def bmi-data (atom {:height 180 :weight 80}))
(def autorefresh {})

(def to-run (atom []))

;; Static component (quiescent-style)

(rum/defc static-timer < rum/static [label ts]
[:div label ": "
[:span {:style {:color @color}} (ts->str ts)]])

(swap! to-run conj #(static-timer "Static" @clock))

;; Raw component (pure react-style)

(rum/defc forced-timer []
[:div "Forced: "
[:span {:style {:color @color}} (ts->str @clock)]])

(swap! to-run conj #(forced-timer))

;; Reactive components (reagent-style)

;; regular static top-down component with immutable args
(rum/defc colored-clock < rum/static [time color]
[:span {:style {:color color}} (ts->str time)])

(rum/defc reactive-timer < rum/reactive []
[:div "Reactive: "
;; Subscribing to atom changes with rum/react
;; Then pass _values_ to static component
(colored-clock (rum/react clock) (rum/react color))])

(swap! to-run conj #(reactive-timer))

;; Reagent stype BMI calculator

(defn calc-bmi [{:keys [height weight bmi] :as data}]
(let [h (/ height 100)]
(if (nil? bmi)
(assoc data :bmi (/ weight (* h h)))
(assoc data :weight (* bmi h h)))))

(defn slider [param value min max]
(let [reset (case param :bmi :weight :bmi)]
[:input {:type "range" :value value :min min :max max
:style {:width "100%"}
:on-change #(swap! bmi-data assoc
param (-> % .-target .-value)
reset nil)}]))

(rum/defc bmi-component < rum/reactive []
(let [{:keys [weight height bmi] :as data} (calc-bmi (rum/react bmi-data))
[color diagnose] (cond
(< bmi 18.5) ["orange" "underweight"]
(< bmi 25) ["inherit" "normal"]
(< bmi 30) ["orange" "overweight"]
:else ["red" "obese"])]
(reset! bmi-data data)
"Height: " (int height) "cm"
(slider :height height 100 220)]
"Weight: " (int weight) "kg"
(slider :weight weight 30 150)]
"BMI: " (int bmi) " "
[:span {:style {:color color}} diagnose]
(slider :bmi bmi 10 50)]]))

(swap! to-run conj #(bmi-component))

;; Generic board utils

(def ^:const board-width 19)
(def ^:const board-height 10)

(defn random-board []
(let [cell-fn #(> (rand) 0.9)
row-fn #(vec (repeatedly board-width cell-fn))]
(vec (repeatedly board-height row-fn))))

(rum/defc board-stats < autorefresh rum/reactive [board renders]
"Renders: " (rum/react renders) [:br]
"Board watches: " (count (.getWatches board)) [:br]
"Color watches: " (count (.getWatches color))])

;; Reactive drawing board

(def rboard (atom (random-board)))
(def rboard-renders (atom 0))

(rum/defc rcell < rum/reactive [x y]
(swap! rboard-renders inc)
(let [cursor (rum/cursor rboard [y x])]
;; each cell subscribes to its own cursor inside a board
;; note that subscription to color is conditional:
;; only if cell is on (@cursor == true),
;; this component will be notified on color changes
[ {:style {:background-color (when (rum/react cursor) (rum/react color))}
:on-mouse-over (fn [_] (swap! cursor not) nil)}]))

(rum/defc art-rboard []
(for [y (range 0 board-height)]
[ {:key y}
(for [x (range 0 board-width)]
;; this is how one can specify React key for component
(-> (rcell x y)
(rum/with-key [x y])))])
(board-stats rboard rboard-renders)])

(swap! to-run conj #(art-rboard))

;; Cursor drawing board

(def board (atom (random-board)))
(def board-renders (atom 0))

;; Cursored mixin will avoid re-rendering if cursors have not
;; changed their value since last rendering

(rum/defc art-cell < rum/cursored [x y cursor]
(swap! board-renders inc)
;; note that color here is not passed via arguments
;; it means it will not be taken into account when deciding on re-rendering
[ {:style {:background-color (when @cursor @color)}
:on-mouse-over (fn [_] (swap! cursor not) nil)}])

;; cursored-watch mixin will setup watches for all IWatchable arguments
(rum/defc artboard < rum/cursored rum/cursored-watch [board]
(for [y (range 0 board-height)
:let [y-cursor (rum/cursor board [y])]]
[ {:key y}
(for [x (range 0 board-width)
:let [x-cursor (rum/cursor y-cursor [x])]]
(-> (art-cell x y x-cursor)
(rum/with-key [x y])))])
(board-stats board board-renders)])

(swap! to-run conj #(artboard board))

;; Main (instead of mounting)

(defn -main [& args]
(doseq [func @to-run]
(pr (func))
@@ -1,8 +1,9 @@
(defproject rum "0.6.0"
:description "ClojureScript wrapper for React"
:license { :name "Eclipse"
:url "" }
:url ""
:description "ClojureScript wrapper for React"
:license { :name "Eclipse"
:url "" }
:url ""
:source-paths ["src" "examples"]

[[org.clojure/clojure "1.7.0" :scope "provided"]
@@ -1,9 +1,10 @@
(ns rum.core
[sablono.compiler :as s]))
[sablono.compiler :as s]
[rum.server :as server]))

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

(defn- parse-defc [xs]
@@ -27,16 +28,23 @@
(defn- compile-body [[argvec & body]]
(list argvec (s/compile-html `(do ~@body))))

(defmacro if-cljs
"Return then if we are generating cljs code and else for Clojure code."
[then else]
(if (:ns &env) then else))

(defn- -defc [render-ctor body]
(let [{:keys [name doc mixins bodies]} (parse-defc body)
render-fn (map compile-body bodies)]
`(def ~name ~doc
(let [render-mixin# (~render-ctor (fn ~@render-fn))
class# (rum.core/build-class (concat [render-mixin#] ~mixins) ~(str name))
ctor# (fn [& args#]
(let [state# (args->state args#)]
(rum.core/element class# state# nil)))]
(with-meta ctor# {:rum/class class#})))))
`(def ~name ~(or doc "")
(let [render-mixin# (~render-ctor (fn ~@(if-cljs render-fn bodies)))
class# (rum.core/build-class (concat [render-mixin#] ~mixins) ~(str name))
ctor# (fn [& args#]
(let [state# (args->state args#)]
(rum.core/element class# state# nil)))]
(with-meta ctor# {:rum/class class#})))))

(defmacro defc
"Defc does couple of things:
@@ -90,4 +98,29 @@
(mapcat (fn [[k v]] [(props k) v])))]
`(rum.core/element (ctor->class ~ctor) (args->state [~@as]) (cljs.core/js-obj ~@ps))))

;;; Server-side rendering support

(def build-class server/build-class)
(def args->state server/args->state)
(def element server/element)
(def render->mixin server/render->mixin)
(def render-state->mixin server/render-state->mixin)
(def render-comp->mixin server/render-comp->mixin)
(def with-key server/with-key)
(def with-ref server/with-ref)

;; included mixins
(def static {})

(defn local [initial & [key]]
(let [key (or key :rum/local)]
{:will-mount (fn [state]
(assoc state key (atom initial)))}))

(def reactive {})
(def react deref)

(defn cursor [ref path]
(atom (get-in @ref path)))
(def cursored {})
(def cursored-watch {})
@@ -3,30 +3,16 @@

(let [last-id (volatile! 0)]
(defn next-id []
(vswap! last-id inc)))
[rum.utils :refer [next-id collect call-all]]))

(defn state [comp]
(aget (.-state comp) ":rum/state"))

(defn id [comp]
(:rum/id @(state comp)))

(defn- collect [fn-key classes]
(->> classes
(map fn-key)
(remove nil?)))

(defn- call-all [state fns & args]
(fn [state fn]
(apply fn state args))

(defn build-class [classes display-name]
(assert (sequential? classes))
(let [init (collect :init classes) ;; state props -> state
@@ -0,0 +1,39 @@
(ns rum.server
(:require [rum.utils :refer [next-id collect call-all]]))

(defn build-class [classes display-name]
(assert (sequential? classes))
(let [init (collect :init classes) ;; state props -> state
will-mount (collect :will-mount classes) ;; state -> state
render (first (collect :render classes)) ;; state -> [dom state]
wrapped-render (reduce #(%2 %1) render (collect :wrap-render classes)) ;; render-fn -> render-fn
props->state (fn [props]
(call-all (:rum/initial-state props) init props))]

(fn [props]
(let [state {:rum/id (next-id)}
state (merge state (props->state props))
state (merge state (call-all state will-mount))
[dom state] (wrapped-render state)]

(defn args->state [args]
{:rum/args args})

(defn element [class state & [props]]
(class (assoc props :rum/initial-state state)))

(defn render->mixin [render-fn]
{ :render (fn [state] [(apply render-fn (:rum/args state)) state]) })

(defn render-state->mixin [render-fn]
{ :render (fn [state] [(apply render-fn state (:rum/args state)) state]) })

(defn render-comp->mixin [render-fn]
{ :render (fn [state] [(apply render-fn (:rum/react-component state) (:rum/args state)) state]) })

(defn with-key [element key]
(assoc-in element [1 :key] key))

(defn with-ref [element ref]
@@ -0,0 +1,17 @@
(ns rum.utils)

(let [last-id (volatile! 0)]
(defn next-id []
(vswap! last-id inc)))

(defn collect [fn-key classes]
(->> classes
(map fn-key)
(remove nil?)))

(defn call-all [state fns & args]
(fn [state fn]
(apply fn state args))

0 comments on commit 5add8df

Please sign in to comment.