Skip to content

Beginner Guide

ChenYong edited this page Aug 17, 2020 · 14 revisions

Want to explore by yourself?

What is Respo?

Respo is a virtual DOM library like React, built with ClojureScript to embrace functional programming.

Before start

Besides experiences on Web apps, you also need to know:

Component definition

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:

(defn comp-space [w h]
  (merge respo.schema/component
    {:args (list w h) ,
     :name :comp-space,
     :render (fn [w h] (div {:style (compute w h)}))}))

So comp-space is a function:

(comp-space nil 16)

DOM properties are divided into style on(events) and attributes. Specify them in HashMaps or nothing:

(input
  {:style {:color "grey"},
   :on-input (on-text-statecursor), ; a function for each event, will explain later
   ; attributes
   {:placeholder "A name"})

Short hands

<> 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)

States

A component can also be created with states, it also need a cursor for updating states:

(defcomp comp-demo [states]
  (let [cursor (:cursor states) ; passing togather with states
        state (or (:data states) {:text ""})] ; setting initial state with `(or nil "")`
    (input {:value (:text state)
            :on-input (fn [e dispatch!]
                          ; will update component state(saved in global store)
                          (dispatch! cursor (assoc state :text (:value e)))})))

(dispatch! cursor new-state) updates state of current component. Internally it's transformed into (dispatch! :states [cursor new-state]) which can be handled in updater function.

Component states are not saved inside components, but as a tree in the store. Suppose store is:

{:states {}}

Use respo.core/>> to specify a new branch of the state tree:

(comp-demo (>> states :demo))

Then the state of comp-demo would be in global store:

{:states {:cursor [], :data {}, :demo {:data {}}}}

Actually it's still {:states {}}, but it's like we got nil when you look into (:demo state).

You need to handle states operation in the store with function respo.cursor/update-states:

(defonce *store (atom {:states {}}))

(defn updater [store op op-data]
  (case op
    :states (update-states store op-data)
    store))

Render to the DOM

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.

Adding effects

To define effects, use defeffect:

(defeffect effect-a [x y] [action el 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.

Rerender on updates

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.

Handling events

To make state update, you need to pass a function to :on-input field. This function will be called with parameters of event(wrapped in :original-event of e), dispatch!(function we defined before). And you also need a cursor:

(input
  {:value (:text task)
   :style style-input
   :on-input (fn [e dispatch!]
                 (dispatch! cursor (:value e)))})

To handle a global action, call dispatch! with an action type and a parameter:

(div
  {:style style-button,
   :on-click (fn [e dispatch!]
                 (dispatch! :remove (:id (:task props))))}
  (<> "Remove"))

dispatch! will cause a change in *store. Also note previously :on was :event.

Composing component

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))})
  (=< 8 nil)
  (input
    {:value (:text task)
     :style style-input
     :on-input (on-text-change props state)})
  (=< 8 nil)
  (div
    {:style style-time}
    (span
      {:inner-text (:time state)
       :style style-time})))

Hooks plugin

Sometimes you need plugins like React hooks for reusing UI with states that can be operated from outside, you can create a plugin:

(defplugin use-counter [states options]
  (let [state (or (:data states) {:value 0})
        cursor (:cursor states)])
  {:ui (div {} (<> (:value state)))
   :add (fn [d!]
          (d! cursor (update state :value inc)))})

Then it can be used like:

(let [counter (use-counter (>> states :counter))])

(:ui counter) ; virtual DOM

((:add counter) d!) ; operation

It's okey if you define a plugin with only a plain function, since it's mostly is a function. defplugin is suggested since this macro provides extra caching mechanism and you might get improvements in performance. Internally it's caching with momof.

Others

Find working examples:

Find me on Twitter if you got interested.

Clone this wiki locally