-
Notifications
You must be signed in to change notification settings - Fork 10
Quick Start
Newer version at Beginner Guide
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 [respo.core :refer [defcomp div]]))
(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.core/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.
To define effects, use defeffect
:
(defeffect effect-a [x y] [action el *local at-place?]
(println "action" action el))
A vector is required to add effects into component:
(defcomp comp-a [a b]
[(effect-a a b)
(div {})])
Effects will be called multiple during moumting, updating and unmounting with different action
value.
Users need to detect and insert effects by need.
Effects can be shared across component, it's just a piece of data. Dispatching actions is not allowed inside effects, which is unlike React.
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.