-
Notifications
You must be signed in to change notification settings - Fork 10
Quick Start
Want to explore by yourself?
- Minimal App https://github.com/Respo/minimal-respo
- Examples https://github.com/Respo/respo-examples
Respo is a virtual DOM library like React, built with ClojureScript to embrace functional programming.
Besides experiences on Web apps, you also need to know:
- Clojure
- ClojureScript http://clojurescript.org
- shadow-cljs https://github.com/thheller/shadow-cljs
Components are defined with a macro called defcomp
:
(defcomp comp-space [w h]
(div {:style {}}))
where div
is a macro for creating virtual element for <div>
.
The full code looks like:
(ns respo.comp.space
(:require-macros [respo.macros :refer [defcomp div]])
(:require [respo.alias :refer [create-comp]]))
(def style-space
{:width "1px", :display "inline-block", :height "1px"})
(defn compute [w h] (if (some? w) (assoc style-space :width w) (assoc style-space :height h)))
(defcomp comp-space [w h]
(div {:style (compute w h)}))
Internally, defcomp
will expand the expression to:
(def comp-space
(create-comp :space
(fn [w h]
(fn [cursor]
(div {:style (compute w h)})))))
So comp-space
is a function:
(comp-space nil 16)
DOM properties are divided into style
event
and attributes. Specify them in HashMaps or nothing:
(input
{:style {:color "grey"},
:on {:input (on-text-state cursor)}, ; a function for each event, will explain later
; attributes
{:placeholder "A name"})
<>
is a macro, like alias:
(<> span text style)
expands to
(span {:inner-text text, :style style})
Being a multiple arity macro, it also supports:
(<> span text)
(<> text)
=<
is an alias for comp-space
, just use it like that:
(=< 8 nil) ; (comp-space 8 nil)
A component can also be created with states.
cursor
is inserted with macro defcomp
so no worries.
(defn on-input [old-state cursor]
(fn [e dispatch! mutate!]
(mutate! (:value e)))) ; will mutate component state(saved in global store)
(defcomp comp-demo [states]
(let [state (or (:data states) "")] ; setting initial state with `(or nil "")`
(input {:value state
:on {:input (on-input state cursor)}}))
(mutate! s)
updates state of current component.
To update parents' states, use (mutate! cursor s)
with a right cursor
(advanced topic).
Component states are not saved locally with components, but as a tree inside the store. Suppose store is:
{:states {}}
Use respo.macros/cursor->
to specify a new branch of the state tree:
(cursor-> :demo comp-demo states)
Then the state of comp-demo
would be in global store:
{:states {:demo nil}}
Actually it's still {:states {}}
, but it's like we got nil
when you look into (:demo state)
.
You also need respo.cursor/mutate
to update state tree in the store:
(defonce *store (atom {:states {}}))
(defn updater [store op op-data op-id]
(case op
:states (update store :states (mutate op-data))
store))
The op-data
in the example means "path" or "branch".
In order to render, you need to define store
and states
.
Use Atoms here since they are the data sources that change over time:
(defonce *store (atom {:states {}}))
(defn id! [] (.valueOf (js/Date.))
(defn dispatch! [op op-data]
(let [op-id (id!))
new-store (updater @*store op op-data op-id)]
(reset! *store new-store)))
(def mount-target (.querySelector js/document "#app"))
(defn render-app! []
(let [app (comp-container @*store)]
(render! mount-target app dispatch!)))
Note that you need to define dispatch!
function by yourself.
Better to render on page load and changes of data sources:
(defn main! []
(render-app!)
(add-watch global-store :rerender render-app!))
(set! (.-onload js/window) main!)
To cooperate with hot swapping:
(defn reload! []
(clear-cache!)
(render-app!))
Notice that clear-cache!
is from respo.core
and it clears component caches after code updated.
Caching is a mechanism to speed up virtual DOM rendering. It's invalidated after code changes.
To make state update, you need to pass a function to :input
field in :on
.
This function will be called with parameters of event
(wrapped in :original-event
of e
), dispatch!
(function we defined before), mutate!
. mutate!
changes a component state:
(defn on-text-change [old-state]
(fn [e dispatch! mutate!]
(mutate! (:value e))))
(input
{:value (:text task)
:style style-input
:on {:input (on-text-change old-state)}})
To handle a global action, call dispatch!
with an action type and a parameter:
(defn handle-remove [props]
(fn [e dispatch! mutate!]
(dispatch! :remove (:id (:task props)))))
(div
{:style style-button,
:on {:click (handle-remove props)}}
(<> span "Remove" nil))
dispatch!
will cause a change in *store
. Also note previously :on
was :event
.
Reusing components is easy. They are wrapped functions that return components:
(div
{:style style-task}
(comp-debug task {:left "160px"})
(button {:style (style-done (:done task))})
(comp-space 8 nil)
(input
{:value (:text task)
:style style-input
:on {:input (on-text-change props state)}})
(comp-space 8 nil)
(div
{:style style-time}
(span
{:inner-text (:time state)
:style style-time})))
You may need a shadow-cljs.edn
to configure the compiler:
{:source-paths ["src"]
:dependencies [[mvc-works/hsl "0.1.2"]
[respo/ui "0.1.9"]
[respo "0.5.0"]]
:builds {:app {:output-dir "target/"
:asset-path "."
:target :browser
:modules {:main {:entries [app.main]}}
:devtools {:after-load app.main/reload!}}}}
then you can compile it:
shadow-cljs watch app
Find the more in https://github.com/Respo/minimal-respo
Create an HTML with <script src="main.js"></script>
in target/
to run it.
http-server target/
I will be on Twitter if you need help.