Skip to content

Quick Start

题叶 edited this page Jul 2, 2016 · 33 revisions

Would like to explore by yourself? Check out this demo: https://github.com/mvc-works/respo-spa-example

What is Respo?

Respo is a simple DOM library like React but totally built with ClojureScript to embrace functional programming.

Before start

Since you have reached this page, I suppose you have some experiences on Web developing. There are quite some stuffs you need to know:

Yeah, I will suggest Boot for its simplicity. Boot is like Gulp. You may also try Figwheel.

Component definition

A component is created by calling create-comp function with a name and a render method, for example:

(ns respo.component.space
  (: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}))

(defn render [w h] (fn [state mutate!] (div {:style (style-space w h)})))

(def comp-space (create-comp :space render))

comp-space here is a function that accepts props.

DOM properties are divided into style event and attrs. Specify them with HashMaps or nothing:

(input
  {:style style-input,
   :event {:input on-text-state},
   :attrs {:value state}})

States

Component can also be created with state methods, like:

(defn init-state [props] {:draft ""})

(defn update-state [old-state changes]
  (merge old-state changes))

(create-comp :demo init-state update-state render)

Both init-state and update are pure functions.

Render to the DOM

In order to render, you need to feed the component with store and states. Better to define them with Atom since they are the data sources:

(defonce store-ref (atom schema/store))
(defonce states-ref (atom {}))

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

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

Rerender on updates

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

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

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

Also by using boot-reload you get hot swapping for free 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 a state update, you need to pass a function to :input field in :event. This function will be called with event(wrapped) and dispatch function we defined in rendering. And at the end of the function body, call mutate with changes:

(defn on-text-state [e dispatch! mutate!]
  (mutate (:value e))))

(input
  {:style style-input,
   :event {:input on-text-change},
   :attrs {:value (:text task)}})

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

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

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

Both dispatch! and mutate will cause a change in store-ref or states-ref.

You may have noticed [state mutate!] in defining render of component. Normally it's useless, however if a child component wants to mutate its parent, use this mutate! instead.

Composing component

Reusing components is easy, they are like just functions that returns components:

(div
  {:style style-task}
  (comp-debug task {:left "160px"})
  (button {:style (style-done (:done task))})
  (comp-space 8 nil)
  (input
    {:style style-input,
     :event {:input (on-text-change props state)},
     :attrs {:value (:text task)}})
  (comp-space 8 nil)
  (div
    {:style style-time}
    (span
      {:style style-time,
       :attrs {:inner-text (:time state)}})))

We may introduce Clojure Spec in the future to validate component paramaters.

Compile and run

You may need a build.boot to run ClojureScript, here's the part related:

(deftask dev []
  (comp
    (watch)
    (reload :on-jsload 'spa-example.core/on-jsload)
    (cljs)
    (target)))

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

boot dev # and wait a minute

Then open target/index.html to see if it works.

Find me on Twitter if you run into problems

Clone this wiki locally