Skip to content

Quick Start

jiyinyiyong edited this page Jun 23, 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 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)}))

; which expands to:
(def comp-space
  (create-comp :space
    (fn [w h]
      (fn [cursor]
        (div {:style (compute 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"})

Short hands

<> is a macro to simplify you simple text:

(<> span text style)

expands to

(span {:inner-text text, :style style})

=< 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. cursor is inserted with macro defcomp so no worries.

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

(defcomp comp-demo [states]
  (let [state (or (:data states) "")]
    (input {:value state
            :event {: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).

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

(cursor-> :demo comp-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 op-id]
  (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 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)))

(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! []
  (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 [old-state]
  (fn [e dispatch! mutate!]
    (mutate! (:value e))))

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

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

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

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

Both dispatch! will cause a change in *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.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.

Clone this wiki locally