Skip to content

Quick Start

题叶 edited this page Jun 12, 2017 · 33 revisions

Want to explore by yourself?

What is Respo?

Respo is a virtual DOM library like React, but 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:

(ns respo.comp.space
  (:require-macros [respo.macros :refer [defcomp]])
  (:require [respo.alias :refer [create-comp div]]))

(defn style-space [w h]
  (if (some? w)
    {:width w, :display "inline-block", :height "1px"}
    {:width "1px", :display "inline-block", :height h}))

(defcomp comp-spac [w h]
  (div {:style (style-space w h)}))

; ; which expands to:
; (def comp-space
;   (create-comp :space
;     (fn [w h]
;       (fn [cursor]
;         (div {:style (style-space w h)})))))

comp-space is a function that accepts parameters like render function:

(comp-space nil "16px")

DOM properties are divided into style event and attributes. Specify them in HashMaps or nothing:

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

States

A component can also be created with states, like:

(defn on-input [old-state cursor]
  (fn [e dispatch!]
    (dispatch! :states [cursor (:value e)])))

(defn comp-demo
  (create-comp :demo
    (fn [states]
      (fn [cursor]
        (let [state (or (:data states) "")]
          (input {:value state
                  :event {:input (on-input state cursor)}}))))

Use respo.cursor/with-cursor to pick the branch of the state tree:

(with-cursor :demo (comp-demo (:demo states)))

You also need respo.cursor/mutate to update the store since states is held in it:

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

(defn updater [store op op-data]
  (case op
    :states (update store :states (mutate 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 schema/store))

(defn dispatch! [op op-data]
  (let [op-id (.valueOf (js/Date.))
        new-store (updater @*store op op-data op-id)]
    (reset! ref-store new-store)))

(defn render-app! []
  (let [mount-target (.querySelector js/document "#app")
        app (comp-container @*store)]
    (render! app mount-target dispatch!)))

Note that you need to define dispatch! function by yourself.

Rerender on updates

Better to render on page load and changes of data sources:

(defn main! []
  (enable-console-print!)
  (render-app!)
  (add-watch global-store :rerender render-app!))

(set! (.-onload js/window) main!)

Also by using boot-reload plugin you get hot swapping easily with store and states retained:

(defn on-jsload! []
  (clear-cache!)
  (render-app!))

Notice that clear-cache! is from respo.core and it clears component caches after code updated.

Handling events

To handle state updates, you need to pass a function to :input field in :event. This function will be called with event(wrapped in :original-event of e) and dispatch! function we defined in rendering. To change component state:

(defn on-text-change [state cursor]
  (fn [e dispatch!]
    (dispatch! :states [cursor (:value e)]))))

(input
  {:value (:text task)
   :style style-input
   :event {:input (on-text-change state cursor)}})

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

(defn handle-remove [props]
  (fn [e dispatch!]
    (dispatch! :remove (:id (:task props)))))

(div
  {:style style-button,
   :event {:click (handle-remove props)}}
  (span {:inner-text "Remove"}))

Both dispatch! will cause a change in ref-store.

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

Compile and run

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.4.4"]]
 :builds {:app {:output-dir "target/"
                :asset-path "."
                :target :browser
                :modules {:main {:entries [app.main]}}
                :devtools {:after-load app.main/reload!}}}}

Find the whole build.boot at https://github.com/Respo/respo-spa-example

shadow-cljs -b app --dev

Serve an HTML with js files in target/ to run it.

I will be on Twitter if you need help.

Clone this wiki locally