https://github.com/pbaille/serum
composing rum components
https://github.com/pbaille/serum
clj cljs react rum
Last synced: 4 months ago
JSON representation
composing rum components
- Host: GitHub
- URL: https://github.com/pbaille/serum
- Owner: pbaille
- Created: 2016-01-12T07:37:18.000Z (over 10 years ago)
- Default Branch: master
- Last Pushed: 2017-02-02T07:03:05.000Z (over 9 years ago)
- Last Synced: 2025-05-13T03:34:36.150Z (about 1 year ago)
- Topics: clj, cljs, react, rum
- Language: Clojure
- Size: 801 KB
- Stars: 4
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Serum: compose, extend and style rum components

CAUTION! Alpha stage
## Usage
add the following to your dependencies: `[serum "0.1.0-SNAPSHOT"]`
```clojure
(ns my-ns (:require [serum.core :as s]))
```
## examples
scomp function is used to define new components, it takes a map with the following keys:
### :body
```clojure
(mount (scomp {:body ["hello scomp!"]}))
```
the body key should contains a seq representing the body of the component
```clojure
(mount (scomp {:body (fn [state] (println state) ["hello scomp!"])}))
```
it can also hold a function that given the component state return a seq representing the body
### :args
```clojure
(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])
:args {:a "hello!"}}))
```
this makes more sense when we actually need the state to build the body!
by the way we discover another option key named :args that simply hold arbitrary state that we need for our component
###:schema
```clojure
(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])
:schema {:a s/Str}
:args {:a "hello!"}}))
```
you can add a schema to check args
```clojure
(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])
:schema {:a s/Str}
:args {:a 1}}))
```
should throw an exception
```clojure
(def a0 (atom 0))
(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])
:schema {:a (ref s/Int)}
:args {:a a0}}))
(swap! a0 inc)
```
if some args are refs that you want the component be reactive on you can tell it like this,the ref function is just a convenience that return a schema
###:attrs
```clojure
(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])
:attrs (sfn {{a :a} :args} {:on-click (fn [_] (swap! a inc))})
:schema {:a (ref s/Int)}
:args {:a a0}}))
(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])
:attrs (afn {a :a} {:on-click (fn [_] (swap! a inc))})
:schema {:a (ref s/Int)}
:args {:a a0}}))
(comment
"those expressions are equivalent"
(with-meta (fn [{{a :a} :args}] "body") {:type :sfn})
(sfn {{a :a} :args} "body")
(afn {a :a} "body"))
```
the attrs option is used to provide one or many attribute-constructor(s) or attribute-map(s),
an attribute constructor is a fn that olds {:type :sfn} in metadata and return an attribute-map,
sfn stands for 'state function' in other words a value that depends on the component state
it can be built with sfn or afn macros (note that first argument is a binding form, for sfn it binds on full state and for afn on args)
```clojure
(mount (scomp {:body (afn {a :a} [[:div (rum/react a)]])
:attrs (afn {a :a} {:on-click (fn [_] (swap! a inc))})
:schema {:a (ref s/Int)}
:args {:a a0}}))
```
the `afn` macro provide a cleaner way to declare constructors that cares only about args
```clojure
(mount (scomp {:body (afn {a :a} [[:div (rum/react a)]])
:attrs [(afn {a :a} {:on-click (fn [_] (swap! a inc))})
{:on-mouse-over (fn [e] (println e))}]
:schema {:a (ref s/Int)}
:args {:a a0}}))
```
the :attrs option can take several attributes-constructors or attributes-map at a time
###:style
```clojure
(mount (scomp {:body (afn {t :text} [[:p t]])
:style (afn {c :color} {:background-color c})
:args {:color :lightskyblue :text "Hello!"}}))
```
you can specify styles in the same way than attrs.
```clojure
(def ss1 {:background-color :tomato
:padding :5px
:border-radius :5px
:border "3px solid lightcyan"})
(def c1
(scomp {:body (afn {t :text} [[:p t]])
:style [ss1 (afn {c :color} {:background-color c})]
:args {:color :lightskyblue :text "Hello!"}}))
(mount c1)
```
like attrs it can take several at a time (constructors or maps).
###:bpipe
bpipe option can hold one or severals body transformations (body -> body)
after the body has been evaluated, it is passed in all body transformations
```clojure
(mount (scomp {:body [c1 [c1 {:args {:text "goodbye!"}}]]
:bpipe (fn [b] (apply concat (repeat 3 b)))}))
(mount [c1 {:bpipe (fn [b] (repeat 3 (first b)))}])
```
###:mixins
```clojure
(def polite-comp
{:did-mount (fn [_] (println "Hello!"))})
(mount (scomp {:mixins [polite-comp]
:body ["yop"]}))
```
you can provide mixins, just like in rum
### hiccup like vector litterals
Once your component is attached to a var, you can use it like this:
```clojure
(mount [c1])
```
You can pass it an extension map, that will be merged into existing configuration.
```clojure
(mount [c1 {:args {:color :mediumaquamarine}}])
```
you can provide `:args`, `:attrs`, `:style`, `:bpipe`, `:body`, `:schema` as in your `scomp` call
```clojure
(mount [c1 {:attrs {:on-click (fn [_] (println "yo"))}}])
```
we are just adding a click handler here.
### injections, selectors
```clojure
(def c2 (scomp {:wrapper :.foo
:body (afn {c :content} c)}))
(def c3 (scomp {:body [[c2 {:args {:content "foo"}}]
[c2 {:args {:content "bar"}}]]
:attrs {($ ".foo") {:on-click (fn [_] (println "injected handler"))}}
:style {:background-color :purple
:padding :10px
($ ".foo") {:background-color :lightcoral
:font-size :25px
:color :white
:padding :10px}}}))
(mount c3)
```
you can inject styles or attributes into sub components via selectors.
there's a bunch of built in selectors, for matching subcomponents.
```clojure
$e ;;tag selector
$k ;;class selector
$id ;;id selector
$ ;;wild selector
($ "#yo") ($ ".foo") ($ "div")
$childs ;;matches all subcomponents
$p ;predicate selector
($p pred)
;; matches all element that return truthy when passed to pred
$and
($or ($id "yo") ($k ".foo"))
;; matches element that have both id "yo" or class "foo"
$or ;or selector
($or ($id "yo") ($k ".foo"))
;; matches element that have either id "yo" or class "foo"
$not
($not ($k ".foo"))
;; matches all subcomponents without class "foo"
$nth
($nth 1 ($k ".foo"))
;; matches the second subcomponent of class "foo"
```
You can easily implement your own, see $or implementation:
```clojure
(defn $or [& xs]
(selector [c s f]
(let [[match? sels]
(loop [ret false [x & nxt] xs sels []]
(if-not x [ret sels]
(let [[_ s match?] (x c s f)]
(recur (or match? ret) nxt (conj sels s)))))]
[(<<$ (if match? (f c) c) [s f]) (apply $or sels) match?])))
```
TODO, explain this
### css pseudos classes
```clojure
(mount [c3 {:style {:border-radius :5px
:hover {:background-color :pink}}}])
```
you can specify :hover :active and :focus styles like this
### attribute and styles merging
```clojure
(def c4 (scomp {:body ["click me and watch console"]
:attrs {:on-click (fn [_] (println "clicked"))}}))
(mount [c4 {:attrs {:on-click (fn [_] (println "clicked overiden"))}}])
```
when doing this the old click event is overiden by the new
```clojure
(mount [c4 {:attrs (m> {:on-click (fn [_] (println "clicked overiden"))})}])
```
with m> it is added
```clojure
(def default-on-click (m? {:on-click (fn [_] (println "default click"))}))
(def c5 (scomp {:body ["click me"]}))
(mount [c5 {:attrs default-on-click}])
```
when wrap with m? an attribute or style is merged only if not present in the target component
```clojure
(mount [c4 {:attrs default-on-click}])
```
should not change c4 click
```clojure
(def wrap-click
(m! {:on-click
(fn [click-handler]
(fn [_] (println "wrap") (click-handler) (println "wrap...")))}))
(mount [c4 {:attrs wrap-click}])
```
with m! you can swap an attribute value
## Usage
```clojure
(def atom1 (atom 1))
(def atom2 (atom 10))
(def atom3 (atom {:a 12 :b 13}))
(def c1
(scomp {:label :c1
:wrapper :div#aze.ert
:attrs [{:on-click (fn [_] (println "yop"))}
{:on-mouse-over (fn [_] (println "over"))}
(m> (afn {b :b} {:on-click (fn [_] (swap! b inc))}))]
:style [{:background-color :mediumaquamarine
:padding (str "10px")
:border (str "10px solid grey")
:hover {:background-color :mediumslateblue}
:active {:background-color :pink}}
(afn {a :a b :b}
{:margin (str a "px")
:padding (str @b "px")})]
:body (fn [_] (rum/react b) ["Hello scomp!"])
:schema {:a s/Int :b (ref s/Int)}
:args {:a 50 :b (cursor atom3 [:b])}}))
(mount [c1 {:style (afn {a :a} {:border (str (/ a 4) "px solid lightskyblue")})
:attrs (m> {:on-click (fn [_] (println "yep"))})
:args {:a 12}
:bpipe (fn [b] (conj b [:div "one"]))}])
(def c2
(scomp {:wrapper :.qsd
:label :c2
:body [c1 c1]}))
(def c3
(scomp {:body [c2 c2]
:style {($ :.qsd) (m? {:border "10px solid lightgrey"})
($and ($ :.ert) ($ :$c1)) {:hover {:background-color :lightcoral}}
($or ($ :.zup) ($ :$c1)) {:color :white}
($p #(= :c1 (:label %))) {:font-size :30px}
($and ($ :$c2) ($nth 1 ($ :$c2))) {:background-color :lightcyan}
($or ($ :$c1) ($nth 1 ($ :$c2))) {:background-color :lightcyan}}}))
(mount c3)
```