{"id":15010638,"url":"https://github.com/pbaille/serum","last_synced_at":"2026-02-15T01:05:51.837Z","repository":{"id":72549214,"uuid":"49483385","full_name":"pbaille/serum","owner":"pbaille","description":"composing rum components","archived":false,"fork":false,"pushed_at":"2017-02-02T07:03:05.000Z","size":820,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-13T03:34:36.150Z","etag":null,"topics":["clj","cljs","react","rum"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pbaille.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-01-12T07:37:18.000Z","updated_at":"2022-10-01T03:00:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"c6c4152a-0c9e-4765-afee-77c91efccd56","html_url":"https://github.com/pbaille/serum","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pbaille/serum","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbaille%2Fserum","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbaille%2Fserum/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbaille%2Fserum/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbaille%2Fserum/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pbaille","download_url":"https://codeload.github.com/pbaille/serum/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbaille%2Fserum/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29463612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-15T01:01:38.065Z","status":"ssl_error","status_checked_at":"2026-02-15T01:01:23.809Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clj","cljs","react","rum"],"created_at":"2024-09-24T19:35:11.047Z","updated_at":"2026-02-15T01:05:51.820Z","avatar_url":"https://github.com/pbaille.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Serum: compose, extend and style rum components\n\n![alt tag](https://github.com/pbaille/serum/blob/master/resources/public/syringe.png)\n\nCAUTION! Alpha stage  \n\n## Usage \n\nadd the following to your dependencies: `[serum \"0.1.0-SNAPSHOT\"]`\n\n```clojure\n(ns my-ns (:require [serum.core :as s]))\n```\n\n## examples\n\nscomp function is used to define new components, it takes a map with the following keys:  \n\n### :body\n\n```clojure\n(mount (scomp {:body [\"hello scomp!\"]}))\n```\n\nthe body key should contains a seq representing the body of the component\n\n```clojure\n(mount (scomp {:body (fn [state] (println state) [\"hello scomp!\"])}))\n```\n\nit can also hold a function that given the component state return a seq representing the body\n  \n### :args\n\n```clojure\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])\n               :args {:a \"hello!\"}}))             \n```\n\nthis makes more sense when we actually need the state to build the body!\nby the way we discover another option key named :args that simply hold arbitrary state that we need for our component\n  \n###:schema\n\n```clojure\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])\n               :schema {:a s/Str}\n               :args {:a \"hello!\"}}))\n```\nyou can add a schema to check args\n\n```clojure\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div a]])\n               :schema {:a s/Str}\n               :args {:a 1}}))\n               \n``` \n\nshould throw an exception\n  \n```clojure\n(def a0 (atom 0))\n\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])\n               :schema {:a (ref s/Int)}\n               :args {:a a0}}))\n\n(swap! a0 inc)\n\n```\n\nif 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\n  \n  \n###:attrs\n\n```clojure\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])\n               :attrs (sfn {{a :a} :args} {:on-click (fn [_] (swap! a inc))})\n               :schema {:a (ref s/Int)}\n               :args {:a a0}}))\n\n(mount (scomp {:body (fn [{{a :a} :args}] [[:div (rum/react a)]])\n               :attrs (afn {a :a} {:on-click (fn [_] (swap! a inc))})\n               :schema {:a (ref s/Int)}\n               :args {:a a0}}))\n\n(comment\n  \"those expressions are equivalent\"\n  (with-meta (fn [{{a :a} :args}] \"body\") {:type :sfn})\n  (sfn {{a :a} :args} \"body\")\n  (afn {a :a} \"body\"))\n```\n\nthe attrs option is used to provide one or many attribute-constructor(s) or attribute-map(s),\nan attribute constructor is a fn that olds {:type :sfn} in metadata and return an attribute-map,\nsfn stands for 'state function' in other words a value that depends on the component state\nit 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)  \n\n```clojure\n(mount (scomp {:body (afn {a :a} [[:div (rum/react a)]])\n               :attrs (afn {a :a} {:on-click (fn [_] (swap! a inc))})\n               :schema {:a (ref s/Int)}\n               :args {:a a0}}))                \n``` \n\nthe `afn` macro provide a cleaner way to declare constructors that cares only about args\n\n```clojure\n(mount (scomp {:body (afn {a :a} [[:div (rum/react a)]])\n               :attrs [(afn {a :a} {:on-click (fn [_] (swap! a inc))})\n                       {:on-mouse-over (fn [e] (println e))}]\n               :schema {:a (ref s/Int)}\n               :args {:a a0}}))\n``` \n\nthe :attrs option can take several attributes-constructors or attributes-map at a time\n\n###:style\n  \n```clojure\n(mount (scomp {:body (afn {t :text} [[:p t]])\n               :style (afn {c :color} {:background-color c})\n               :args {:color :lightskyblue :text \"Hello!\"}}))\n```\n\nyou can specify styles in the same way than attrs.\n\n```clojure\n(def ss1 {:background-color :tomato\n          :padding :5px\n          :border-radius :5px\n          :border \"3px solid lightcyan\"})\n\n(def c1\n  (scomp {:body (afn {t :text} [[:p t]])\n          :style [ss1 (afn {c :color} {:background-color c})]\n          :args {:color :lightskyblue :text \"Hello!\"}}))\n\n(mount c1)\n\n```\n\nlike attrs it can take several at a time (constructors or maps).\n\n###:bpipe\n\nbpipe option can hold one or severals body transformations (body -\u003e body)\nafter the body has been evaluated, it is passed in all body transformations\n\n```clojure\n(mount (scomp {:body [c1 [c1 {:args {:text \"goodbye!\"}}]]\n               :bpipe (fn [b] (apply concat (repeat 3 b)))}))\n\n(mount [c1 {:bpipe (fn [b] (repeat 3 (first b)))}])\n\n```\n\n###:mixins\n\n```clojure\n(def polite-comp\n  {:did-mount (fn [_] (println \"Hello!\"))})\n\n(mount (scomp {:mixins [polite-comp]\n               :body [\"yop\"]}))\n```\n\nyou can provide mixins, just like in rum\n  \n### hiccup like vector litterals\n\nOnce your component is attached to a var, you can use it like this:  \n\n```clojure\n(mount [c1])\n```\n\nYou can pass it an extension map, that will be merged into existing configuration.\n\n```clojure\n(mount [c1 {:args {:color :mediumaquamarine}}])\n```\n\nyou can provide `:args`, `:attrs`, `:style`, `:bpipe`, `:body`, `:schema` as in your `scomp` call\n\n```clojure\n(mount [c1 {:attrs {:on-click (fn [_] (println \"yo\"))}}])\n```\n\nwe are just adding a click handler here.\n   \n### injections, selectors  \n\n```clojure\n(def c2 (scomp {:wrapper :.foo\n                :body (afn {c :content} c)}))\n\n(def c3 (scomp {:body [[c2 {:args {:content \"foo\"}}]\n                       [c2 {:args {:content \"bar\"}}]]\n                :attrs {($ \".foo\") {:on-click (fn [_] (println \"injected handler\"))}}\n                :style {:background-color :purple\n                        :padding :10px\n                        ($ \".foo\") {:background-color :lightcoral\n                                    :font-size :25px\n                                    :color :white\n                                    :padding :10px}}}))\n\n(mount c3)\n```\n\nyou can inject styles or attributes into sub components via selectors.\n\nthere's a bunch of built in selectors, for matching subcomponents.\n\n```clojure\n$e ;;tag selector\n\n$k ;;class selector\n\n$id ;;id selector\n\n$ ;;wild selector\n($ \"#yo\") ($ \".foo\") ($ \"div\")\n\n$childs ;;matches all subcomponents\n\n$p ;predicate selector\n($p pred) \n;; matches all element that return truthy when passed to pred\n\n$and\n($or ($id \"yo\") ($k \".foo\")) \n;; matches element that have both id \"yo\" or class \"foo\"\n\n$or ;or selector\n($or ($id \"yo\") ($k \".foo\")) \n;; matches element that have either id \"yo\" or class \"foo\"\n\n$not \n($not ($k \".foo\")) \n;; matches all subcomponents without class \"foo\"\n\n$nth \n($nth 1 ($k \".foo\")) \n;; matches the second subcomponent of class \"foo\"\n```  \n\nYou can easily implement your own, see $or implementation: \n\n```clojure\n(defn $or [\u0026 xs]\n  (selector [c s f]\n            (let [[match? sels]\n                  (loop [ret false [x \u0026 nxt] xs sels []]\n                    (if-not x [ret sels]\n                              (let [[_ s match?] (x c s f)]\n                                (recur (or match? ret) nxt (conj sels s)))))]\n              [(\u003c\u003c$ (if match? (f c) c) [s f]) (apply $or sels) match?])))\n```\n\nTODO, explain this\n\n### css pseudos classes\n\n```clojure\n  (mount [c3 {:style {:border-radius :5px\n                      :hover {:background-color :pink}}}])\n```\n\nyou can specify :hover :active and :focus styles like this\n\n### attribute and styles merging\n\n```clojure\n(def c4 (scomp {:body [\"click me and watch console\"]\n                :attrs {:on-click (fn [_] (println \"clicked\"))}}))\n\n(mount [c4 {:attrs {:on-click (fn [_] (println \"clicked overiden\"))}}])\n```\n\nwhen doing this the old click event is overiden by the new\n\n```clojure\n(mount [c4 {:attrs (m\u003e {:on-click (fn [_] (println \"clicked overiden\"))})}])\n```\n\nwith m\u003e it is added\n\n```clojure\n(def default-on-click (m? {:on-click (fn [_] (println \"default click\"))}))\n\n(def c5 (scomp {:body [\"click me\"]}))\n\n(mount [c5 {:attrs default-on-click}])\n```\n\nwhen wrap with m? an attribute or style is merged only if not present in the target component\n\n```clojure\n(mount [c4 {:attrs default-on-click}])\n\n```\n\nshould not change c4 click\n\n```clojure\n(def wrap-click\n  (m! {:on-click\n       (fn [click-handler]\n         (fn [_] (println \"wrap\") (click-handler) (println \"wrap...\")))}))\n\n(mount [c4 {:attrs wrap-click}])\n```\n\nwith m! you can swap an attribute value\n  \n## Usage\n\n```clojure\n(def atom1 (atom 1))\n(def atom2 (atom 10))\n(def atom3 (atom {:a 12 :b 13}))\n\n(def c1\n  (scomp {:label :c1\n          :wrapper :div#aze.ert\n          :attrs [{:on-click (fn [_] (println \"yop\"))}\n                  {:on-mouse-over (fn [_] (println \"over\"))}\n                  (m\u003e (afn {b :b} {:on-click (fn [_] (swap! b inc))}))]\n          :style [{:background-color :mediumaquamarine\n                   :padding (str \"10px\")\n                   :border (str \"10px solid grey\")\n                   :hover {:background-color :mediumslateblue}\n                   :active {:background-color :pink}}\n                  (afn {a :a b :b}\n                       {:margin (str a \"px\")\n                        :padding (str @b \"px\")})]\n          :body (fn [_] (rum/react b) [\"Hello scomp!\"])\n          :schema {:a s/Int :b (ref s/Int)}\n          :args {:a 50 :b (cursor atom3 [:b])}}))\n\n(mount [c1 {:style (afn {a :a} {:border (str (/ a 4) \"px solid lightskyblue\")})\n            :attrs (m\u003e {:on-click (fn [_] (println \"yep\"))})\n            :args {:a 12}\n            :bpipe (fn [b] (conj b [:div \"one\"]))}])\n\n(def c2\n  (scomp {:wrapper :.qsd\n          :label :c2\n          :body [c1 c1]}))\n\n(def c3\n  (scomp {:body [c2 c2]\n          :style {($ :.qsd) (m? {:border \"10px solid lightgrey\"})\n                  ($and ($ :.ert) ($ :$c1)) {:hover {:background-color :lightcoral}}\n                  ($or ($ :.zup) ($ :$c1)) {:color :white}\n                  ($p #(= :c1 (:label %))) {:font-size :30px}\n                  ($and ($ :$c2) ($nth 1 ($ :$c2))) {:background-color :lightcyan}\n                  ($or ($ :$c1) ($nth 1 ($ :$c2))) {:background-color :lightcyan}}}))\n\n(mount c3)\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpbaille%2Fserum","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpbaille%2Fserum","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpbaille%2Fserum/lists"}