Quick Start

题叶 edited this page Nov 12, 2016 · 10 revisions
Clone this wiki locally

Would like to explore by yourself? Check out the demos https://github.com/Respo/respo-spa-example

What is Respo?

Respo is a front-end MVC library like React.js but built with ClojureScript to embrace functional programming.

Before start

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

I suggest Boot which is like Gulp. You may also try Figwheel and Leiningen.

Component definition

A component is defined by calling create-comp function with a name and a render method:

(ns respo.comp.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 is a function that accepts parameters like render function:

(comp-space nil "16px")

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

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

States

A component can also be created with states, 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-stateshould be pure functions. And you will use mutate! to trigger state updates.

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

states are used by Respo internally, make sure it's {} in an Atom.

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 :gc (fn [] (gc-states! global-states)))
  (add-watch global-store :rerender render-app!)
  (add-watch global-states :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. Call mutate! to change component state:

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

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

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 {:attrs {:inner-text "Remove"}}))

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

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
    {: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/Respo/respo-spa-example

boot dev # and wait a minute

Run target/index.html to see if it works.

Find me on Twitter if you run into problems