CAUTION! Alpha stage
add the following to your dependencies: [serum "0.1.0-SNAPSHOT"]
(ns my-ns (:require [serum.core :as s]))
scomp function is used to define new components, it takes a map with the following keys:
(mount (scomp {:body ["hello scomp!"]}))
the body key should contains a seq representing the body of the component
(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
(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
(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])
:schema {:a s/Str}
:args {:a "hello!"}}))
you can add a schema to check args
(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])
:schema {:a s/Str}
:args {:a 1}}))
should throw an exception
(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
(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)
(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
(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
(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.
(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
(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
(def polite-comp
{:did-mount (fn [_] (println "Hello!"))})
(mount (scomp {:mixins [polite-comp]
:body ["yop"]}))
you can provide mixins, just like in rum
Once your component is attached to a var, you can use it like this:
(mount [c1])
You can pass it an extension map, that will be merged into existing configuration.
(mount [c1 {:args {:color :mediumaquamarine}}])
you can provide :args
, :attrs
, :style
, :bpipe
, :body
, :schema
as in your scomp
call
(mount [c1 {:attrs {:on-click (fn [_] (println "yo"))}}])
we are just adding a click handler here.
(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.
$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:
(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
(mount [c3 {:style {:border-radius :5px
:hover {:background-color :pink}}}])
you can specify :hover :active and :focus styles like this
(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
(mount [c4 {:attrs (m> {:on-click (fn [_] (println "clicked overiden"))})}])
with m> it is added
(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
(mount [c4 {:attrs default-on-click}])
should not change c4 click
(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
(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)