Respo is a virtual DOM library in Calcit-js

Previously made in ClojureScript.

Inspired by React and Reagent.

Talk to community.

Want to explore by yourself?

What is Respo?

Respo is a virtual DOM library like React, built with Calcit-js 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"
    ; a function for each event, will explain later
    :on-input $ on-text-statecursor
    ; attributes
    :placeholder "|A name"

Short hands

<> is a macro, like alias:

<> text style

expands to

span $ {}
  :inner-text text
  :style style

Being a multiple arity macro, it also supports:

<> 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
      ; "passing togather with states"
      cursor (:cursor states)
      ; "setting initial state with `(or nil |)`"
      state (or (:data states) {:text "|"})]
    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:

defatom *store $ {}
  :states $ {}

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

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:

defatom *store $ {}
  :states $ {}

defn id! () (.!valueOf (new 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 (js/document.querySelector "|#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

Others

Find working examples:

Find me on Twitter if you got interested.

Guide

See posts.

Why Respo?

There are quite some alternatives actually: Reagent, Om, React.js , Deku, or Rum. I think Om is too complicated for me.

The different part of Respo is, it's built in Calcit-js with persistent data structure from scratch. It can be too simple for real world apps that requires lots of side effects, but it is fine to build medium size apps and do experiments about MVC. Respo is designed carefully to maintain global store and global states, as a result hot code swapping is more predictable.

For short, it's simpler, meanwhile purer.

Pros

  • Pure CojureScript, immutable by default with optimizations
  • Global states, hot swapping
  • Flexible HTML DSL with Calcit-js
  • Fewer side effects, less position to make mistakes
  • Flexible component state

Cons

  • Very few components to use
  • Users need to handle life-cycles manually
  • No support for animations
  • Not as fast as React.js

DOM elements

An element is defined with create-element like:

defmacro a (props & children)
  quasiquote
    create-element :a ~props ~@children

Where children comes with keys since Respo always need keys(in keyword, string, or number) to compare children:

[]
  [] 1 (span $ {})
  [] 2 (span $ {})

And an element created like:

input $ {}
  :placeholder "|Pick a name, and hit Enter"
  :on-keydown (fn (e dispatch!))
  :style $ {}
    :line-height 2
    :width "|100%"

might be rendered to an element with events bound:

<input placeholder="Pick a name, and hit Enter"
       style="line-height:2;width:100%;">

Internally an element is stored with EDN like:

{}
  :name tag-name
  :coord nil
  :attrs attrs
  :style styles
  :event event
  :children children

Some of the frequently used elements are defined in respo.core:

a body br button canvas code div footer
  h1 h2 head header html hr img input li link
  option p pre script section select span style textarea title
  ul

Some are not, but you can create them very quickly with create-element.

DOM properties

Respo is updating DOM properties with a simple solution. It's okay but not that friendly. Here are some example on the name mapping:

  • className ->:class-name
  • innerText ->:inner-text
  • innerHTML ->:innerHTML
  • value ->:value

I'm afraid you have to figure out more by yourself.

Properties(except for style and event) are specified in attrs field. style is a HashMap. event is followed with a HashMap of events too.

The impelementation details is:

defn replace-prop (target op)
  let
      prop-name (dashed->camel (name (key op)))
      prop-value (val op)
    if (= prop-name "value")
      if
        not= prop-value (.-value target)
        aset target prop-name prop-value
      aset target prop-name prop-value

defn add-prop (target op)
  let
      prop-name $ dashed->camel (name (key op))
      prop-value $ val op
    case-default prop-name
      aset target prop-name prop-value
      "|style" $ aset target prop-name (style->string prop-value)

defn rm-prop (target op)
  aset target (dashed->camel (name op)) nil

DOM events

Here is a simple demo handling input events:

input $ {}
  :on-input $ fn (e dispatch!)
    println (:value e)

e is a HashMap with several entries:

def e $ {}
  :type "|input"
  :original-event event

The details:

defn event->edn (event)
  ; js/console.log "|simplify event:" event
  ->
    case-default (.-type event)
      {}
        :msg (str "|Unhandled event: " (.-type event))
        :type (.-type event)
      |click $ {}
        :type :click
      |keydown $ {}
        :key-code (.-keyCode event)
        :type :keydown
      |keyup $ {}
        :key-code (.-keyCode event)
        :type :keyup
      |input $ {}
        :value (aget (.-target event) "|value")
        :type :input
      |change $ {}
        :value (aget (.-target event) "|value")
        :type :change
      |focus $ {}
        :type :focus
   assoc :original-event event

Events are bound directly on the elements for simplicity and consistency. And it stops propagation when event is triggered.

Styles

Styles are represented in HashMap so it's very trival to extend with merge and if:

def style-a $ {}
  :line-height 1.6
  :color (hsl 0 0 80)

def style-b $ merge style-a
  {}
    :font-size "|16px"
  if (> size 0)
    {}
      :font-weight "|bold"

The keys have to be keywords, the values can be either of keywords, numbers or strings. Also I prepared a function called hsl as a helper.

In Respo, style updates are defined with direct accessing to el.style:

defn add-style (target op)
  let
      style-name (dashed->camel (name (key op))
      style-value (val op)
    aset (.-style target) style-name style-value

defn rm-style (target op)
  let
      style-name (dashed->camel (name op))
    aset (.-style target) style-name nil

defn replace-style (target op)
  let
      style-name (dashed->camel (name (key op)))
      style-value (val op)
    aset (.-style target) style-name style-value

For convenience, I collected my frequent used styles in a package called respo-ui. You can find more in the source code.

Static Styles

A macro respo.css/defstyle has been added for add <style>...</style> referred with :class-name. It's less dynamic, which means you cannot pass parameters to styles in this way. It will insert into <head>...</head> a <style>...</style> element. It runs before main! and reload!.

define style:

defstyle style-input $ {}
  "\"$0" $ {} (:font-size |16px)
    :line-height |24px
    :padding "|0px 8px"
    :outline :none
    :min-width |300px
    :background-color $ hsl 0 0 94
    :border :none

$0 will be replace by a string of className. So if you want to add rules for :hover, just write $0:hover.

input $ {} (:placeholder "\"Text")
  :value $ :draft state
  :class-name style-input
  :style $ {}
    :width $ &max 200
      + 24 $ text-width (:draft state) 16 |BlinkMacSystemFont

Internally, a definition of respo.app.comp.task/style-done generates className as style-done__respo_app_comp_task(with help of a new API &get-calcit-running-mode), so it's still unique across files and modules. During hot code swapping, the hashmap will be compared to previous hashmap to decide whether or not update.

Since it's not a GC-based solution, <style>..</style> created before hot code swapping remains in the DOM tree and will alway occupy memory. This solution is far from perfect, but it's supposed to cover current needs in Respo.

Node.js rendering

During HTML rendering in Node.js , styles are collected in a list in respo.css/*style-list-in-nodejs. It's an unstable design but you can get styles from it.

Virtual DOM

There are elements and components before it's actually rendered. After rendered, if all of elements. The definitions of them are:

defrecord Element :name :coord :attrs :style :event :children

defrecord Component :name :effects :tree

defrecord Effect :name :coord :args :method

coord means "coordinate" in Respo, it looks like [] 0 1 3 or even [] 0 0 0 :container 0 0 "|a".

If you define component like this:

div
  {}
    :style $ {}
      :color "|red"
    :class-name "|demo"
    :on-click $ fn (e dispatch!)
  div $ {}

You may get a piece of data in Calcit-js:

#respo.core.Element{:name :div,
                    :coord nil,
                    :attrs ([:class-name "demo"]),
                    :style {:color "red"},
                    :event {:click #object[Function "function (e,dispatch_BANG_){
                                                       return null;
                                                     }"]},
                    :children [[0 #respo.core.Element{:name :div,
                                                      :coord nil,
                                                      :attrs (),
                                                      :style nil,
                                                      :event (),
                                                      :children []}]]}

You may have noticed that in children field it's a vector. There is a 0 indicating it's the first child. And yes internally that's the true representation of children.

As I told, virtual DOM is normal Calcit-js data, you can transform the virtual DOM in the runtime:

defn interpose-borders (element border-style)
  if (contains? element :children)
    update element :children $ fn (children)
      interpose-item ([]) 0 children
        hr $ {}
          :style $ merge default-style border-style

This demo inserts borders among child elements. You can think of more.

defeffect

Add effects:

defeffect effect-a (a b) (action el at-place?)
  case-default action
    do
    :mount (println "|mounted")
    :before-update (println "|before update")
    :update (println "|updated")
    :unmount (println "|will unmount")

defcomp comp-a (x y z)
  []
    effect-a a b
    div {}
      <> "|DEMO"
  • [] a b are arguments. they can also be old arguments during unmounting,
  • action can be :mount :before-update :update or :unmount,
  • el refers to root element of component.
  • at-place? being true if change happen exactly from this component, rather than from parents.

Notice that to add effects into component, we need to use a vector. So it's also possible to add multiple effects here:

[]
  defeffect-a a b
  defeffect-b c d
  div $ {}

Respo is different from React. You can not dispatch action during rendering, or inside effects. So there will be no access to dispatch!, and should not have actions.

Render list

To render a list, you need use respo.core/list-> with children in key/value pairs:

list->
  {}
    :style $ {}
  []
    [] "a" (comp-text "|this is A" nil)
    [] "b" (comp-text "|this is B" nil)

If the tag is :div, you can omit that and just write:

list-> props children

It's common pattern to use -> to transform the list:

list->
  {}
    :class-name "|task-list"
    :style style-list
  -> tasks
    reverse
    map $ fn (task)
      [] (:id task) (task-component task)

Child elements are rendered in the order that items appear in the list. Diffing is not very fast, so don't make the list too large.

Component States

Unlike React, states in Respo is maintained manually for stablility during hot code swapping. At first, states is a HashMap inside the store:

defatom *store {}
  :states $ {}

By design, if states is added, you would a tree:

{}
  :states $ {}
    :data $ {}
    :todolist $ {}
      :data $ {}
        :input "|xyz..."
      "task-1-id" $ {}
        :data $ {}
          :draft "|xxx..."
      "task-2-id" $ {}
        :data $ {}
          :draft "|yyy..."
      "task-2-id" $ {}
        :data $ {}
          :draft "|zzz.."

:data is a special field for holding state of each component. It has to be a tree since Virtual DOM is a tree. You also notice that its structure is simpler than a DOM tree, it only contains states.

respo.core/>> is the "picking branch" function. It also maintains a :cursor field.

When you call (>> states :todolist), you get new states variable for a child component:

{}
  ; "generated cursor, nil at top level"
  :cursor $ [] :todolist
  ; "state at current level"
  :data $ {}
    :input "|xyz..."

  ; states for children
  "task-1-id" $ {}
    :data $ {}
      :draft "|xxx..."
  "task-2-id" $ {}
    :data $ {}
      :draft "|yyy..."
  "task-2-id" $ {}
    :data $ {}
      :draft "|zzz.."

Then you call (>> states "task-1-id") and you get new states for child "task-1":

{}
  ; "generated cursor"
  :cursor $ [] :todolist "|task-1-id"

  ; "state of task-1"
  :data $ {}
    :draft "|xxx..."

For state inside each component, it's nil at first. You want to have an initial state, use or to provide one.

defcomp comp-task (states)
  let
      cursor (:cursor states)
      state $ or (:data states) $ {}
        :draft "|empty"
    (div {}))

By accessing (:data states), you get nil, so &{} :draft "|empty" is used. After there's data in states, you get data that was set.

Then you want to update component s:tate

defcomp comp-task (states)
  let
      cursor (:cursor states)
      state $ or (:data states) $ {}
        :draft "|empty"
    div $ {}
      :on-click $ fn (e dispatch!)
        dispatch! cursor (assoc state :draft "|New state")

So (dispatch! cursor state) sends the new state.

The last step to to update global states with respo.cursor/update-states. Internally (dispatch! cursor op-data) will be transformed to (dispatch! :states ([] cursor op-data)). And then in updater you add:

case-default op
  ; other actions
  do store

  ; "where op-data is [cursor new-state]"
  :states (update-states store op-data)

Let's wrap it. First we have empty states inside store:

{}
  :states $ {}

And it is passed to (comp-todolist (>> states :todolist) data), and then passed to (comp-task (>> states (:id task)) task).

In comp-todolist, (:data states) provides component state, (:cursor states) provides its cursor. Call (dispatch! cursor {:input "|New draft"}) and global store will become:

{}
  :states $ {}
    :todolist $ {}
      :data $ {}
        :input "|New draft"

In comp-task of "task-1", you also get state and cursor, so call (dispatch! cursor {:draft "New text"}) you will get:

{}
  :states $ {}
    :todolist $ {}
      :data $ {}
        :input "|New draft"
      "task-1-id" $ {}
        :data $ {}
          :draft "|New text"

And that's how Respo states is maintained.

Hot swapping

Hot swapping is done by the compiler. What you need to do is to call respo.core/clear-cache! before re-rendering happens:

defn reload! ()
  ; "clear component caches"
  clear-cache!
  ; "rerender DOM tree, I mean, a diff/patch loop"
  render-app!
  println "|Code update."

If you don't, in the next rendering phase old element tree would be used if no argument changes found, which means Respo would still use render functions defined previously.

In Respo, you are asked to define *store explicitly. They the global states of data. As an Atom, the value inside is immutable, but the reference is mutable. During hot swapping, variables defined with defatom will be retained. As a result, component states are persistent even code is swapped:

defatom *store $ atom
  or
    let
        raw $ or
          js/localStorage.getItem "|respo"
          , "|{:data [], :states {}}"
      read-string raw
    , schema/store

Base components

There are some base components for building apps built inside Respo:

respo.comp.space/comp-space
respo.comp.space/=< ; "which is an alias for `comp-space`"
respo.comp.inspect/comp-inspect

Also I got some simple component to help:

Respo components are pure, without side effects.

Component for keydowns

It's tricky to listen to global events since Respo does not allow useEffect or useMounted. Respo added a component for listening to global keydowns:

respo.comp.global-keydown :refer $ comp-global-keydown

comp-global-keydown
  {} $ :disabled-commands (#{} "\"s" "\"p")
  fn (e d!) (js/console.log "\"keydown" e)

Internally it listens events on window and dispatches events to a <span/> element.

For more details please read https://github.com/Respo/ssr-stages

Rendering assumptions

Before talking about Server Side Rendering(SSR), you should know about how Respo mounts and rerenders. There's a Atom called *global-element which represents the virtual DOM of currently rendered HTML content on the page:

defatom *global-element nil

And every time you call render!, it checks if old virtual DOM exists. If exists, it will do patching with rerender-app! rather than mounting:

defn render! (target markup dispatch!)
  if (some? @*global-element)
    rerender-app! target markup dispatch!
    mount-app! target markup dispatch!

What is SSR in Respo?

So SSR is there's already HTML in <div class="app">{"some HTML existed"}</div> and Respo need to patch the DOM in the first screen. And in order to generate the patches, we must prepare an old virtual DOM so that we can call diff function.

And note that the HTML transferred over the network does not bind events, and we need to bind them on client side. Internally there's mute-element function to remove events from virtual DOM.

Server rendering

Virtual DOM can be rendered on a server, use it like in JavaScript.

make-string is the function to render HTML. realize-ssr! is also useful to make first screen look smoother, make sure it's called before render!.

Notice that when rendering on server, events are not bound, internally we use mute-element to remove events before rendering. Without realize-ssr!, render! function will remove existing DOM and mount the whole tree.

realize-ssr! solution

How to prepare that virtual DOM? You have to render that by yourself. Since Respo components are like functions, it's not hard. Read code below:

defatom *store $ {}

def mount-target (js/document.querySelector "|.app")

defn -main ()
  if server-rendered?
    realize-ssr! target
      render-element (comp-container @*store)
      , dispatch!
  render-app!
  add-watch *store :changes render-app!

It can be divided into several steps:

  • call (comp-container store) to create component
  • call (render-element component) to render component to virtual DOM
  • call (realize-ssr! target element dispatch!) to reset *global-element we mentioned above
  • then call render! with (render-app!)

In realize-ssr! we also setup the event listener, and all listeners are finished registering after render! is called, i.e. DOM patching finished.

Extracting CSS defined in Calcit

Respo introduced defstyle macro for generating <style/> tags for more CSS code, which is also required when SSR is performed. Simple way is to read @*style-list-in-nodejs and join them into CSS code. CSS rules are handled inside Respo. A rough demo:

let
    app-html $ make-string
      comp-container $ let
          s schema/store
        assoc reel-schema/reel :base s :store s
    styles $ .join-str @*style-list-in-nodejs (str &newline &newline)

  ;nil

Report bugs

This feature has not beed well tested in real world yet. Submit bugs at https://github.com/Respo/respo/issues

Trouble shooting

Go to on Github issues.

And probably you want to fire a bug.

I have to say Respo is not trying to be a powerful library with all the features. Let's keep it small and useful.

Respo API

User APIs

NamespaceFunction
respo.coredefcomp
div
<>
defeffect
create-element
render!
clear-cache!
realize-ssr!
list->
>>
respo.comp.spacecomp-space or =<
respo.comp.inspectcomp-inspect
respo.render.htmlmake-string

Lower level APIs

Normally you don't need low level APIs, and the basic APIs are enough for building a apps.

I documented the APIs that can be useful. It's possible to discover new features we have't noticed yet.

NamespaceFunction
respo.render.expandrender-app
respo.util.formatpurify-element
mute-element
respo.util.listmap-val
map-with-idx
respo.render.difffind-element-diffs
respo.render.patchapply-dom-changes
respo.controller.clientactivate-instance!
patch-instance!

APIs

map-with-idx
respo.util.list/map-with-idx identity ([] :a :b)
; [] ([] 0 :a) ([] 1 :b)

defcomp

defcomp comp-demo (content)
  div
    {}
      :class-name "|demo-container"
      :style $ {}
        :color :red
    <> content

defcomp is a Macro(https://github.com/Respo/respo.calcit/blob/master/compact.cirru#L1295) transforming code to another function with effects extracted.

div

Here's how you use div macro to create a tree of <div>s:

div
  {}
    :class-name "|example"
    :style $ {}
    :on $ {}
  div $ {}
  div $ {}

Its first argument is a HashMap consists of :style :on and other properties. For property like el.className, you write in :class-name.

Find more in DOM elements.

<>

This macro expands

<> text style

into

span $ {}
  :inner-text text
  :style style

defeffect

Add effects:

defeffect effect-a (a b) (action el at-place?)
  case-default action
    do
    :mount (println "|mounted")
    :before-update (println "|before update")
    :update (println "|updated")
    :unmount (println "|will unmount")

defcomp comp-a (x y z)
  []
    effect-a a b
    div {}
      <> "|DEMO"
  • [] a b are arguments. they can also be old arguments during unmounting,
  • action can be :mount :before-update :update or :unmount,
  • el refers to root element of component.
  • at-place? being true if change happen exactly from this component, rather than from parents.

Notice that to add effects into component, we need to use a vector. So it's also possible to add multiple effects here:

[]
  defeffect-a a b
  defeffect-b c d
  div $ {}

Respo is different from React. You can not dispatch action during rendering, or inside effects. So there will be no access to dispatch!, and should not have actions.

create-element

Function to create virtual element. Pass to it a name, a HashMap, and some children:

defmacro a (props & children)
  create-element :a ~props ~@children

children is normally a list. But in some cases we need dynamic children element, then it can be a HashMap.

render!

render! comes with side effects, it renders virtual to the mount pointer:

render! target
  comp-container @global-store
  , dispatch!

target is the mount pointer. global-states is the reference to the atom of states.

Internally there's a mutable state tracking the DOM state. And the state realize-ssr! changes is this one.

clear-cache!

clear-cache!

Respo has two copies of caches inside for the purpose of:

  1. DOM diffing
  2. speed up rendering

The first one is easy to understand since virtual DOM is corresponding to the DOM we currently see in the page. That's why it's cached.

The second one is for rendering, maybe I should explain it in shouldComponetUpdate-like process. Most times this virtual DOM is same with the previous one for diffing. But during hot code swapping, it's not. DOM state is updated, however, caches should be removed. That's why there's a second one.

clear-cache! is to clear the second cache, during hot swapping.

realize-ssr!

This one is complicated. I wrote a long post before trying to explain this new feature. The code looks like this:

def ssr? (some? (.!querySelector js/document "|meta.respo-ssr"))
def mount-target (.!querySelector js/document "|.app"))

if ssr?
  realize-ssr! mount-target
    comp-container @*store ssr-stages
    , dispatch!

The job of realize-ssr! function is to simulate a virtual DOM of currently rendered HTML from server, so that the followed virtual DOM rendering steps can run a little easier.

list->

A macro for rendering lists:

list->
  {}
    :style $ {}
  [] 1 (<> "|1")
  [] 2 (<> "|2")

The first argument should be a keyword. The last argument should be a collection.

>>

Creating a branch of states, as well as a new cursor:

comp-x (>> states branch-key) p1 p2

Notice that the structure of states is a tree of data and cursor:

{}
  :cursor $ [] :a
  :data $ {}

  :b $ {}
    :data $ {}
      :c $ {}
        :data $ {}

When (>> states :b) is evaluated, new piece of data is generated:

{}
  :cursor $ [] :a :b
  :c $ {}
    :data $ {}

In an older version, cursor and states are two separated variables, but now combined for convenience.

comp-space

This is the component to add spaces. It decouples whitespaces from margins of elements, so I consider it a good practice.

; "for horizontal space"
comp-space 8 nil

; "for vertical space"
comp-space nil 8

Make sure that one of them left nil so the component may fill it.

It's also okay to use strings:

; for vertical space
comp-space nil "|8px"

The bad aspect is every <div> actually costs. And margin is still an alternative solution.

Notice, marge is more performant than an extra element. Make you choice when you feel your app is slower.

comp-inspect

This component is similiar to comp-text, it shows data in string with styles. The differeces is, x can be anything type(formatted with pr-str):

comp-inspect "|a tip" x
  {}
    :color "|red"

This component also comes with special styles to show it in an absolute position. When clicked, it will print data with (to-js-data x).

Disable component in production:

comment (comp-inspect x nil)

make-string

Generate HTML from a virtual DOM. Stringified HTML contains a lot of markups, such as :data-coord:

make-string (div $ {})
; "<div data-coord=\"[]\" data-event=\"#{}\"></div>"

It's quite limited, so the only use case is server side rendering. It doesn't have to be a real web server, scripts on Node.js is also fine.

And it's also called "universal rendering", or I would call it "progressive rendering". The rendering process started in Gulp, then in server, and last last in a browser. So it's progressive. Respo needs to make sure the HTML is identical rendered anywhere.

render-app

This function renders virtual DOM markups into virtual DOM data:

render-app markup states build-mutate old-element

old-element is a caches of the old virtual DOM. It can help speed up rendering since arguments and results are cached.

Each component comes with a render function defined with defcomp. So those render functions needs to be rendered with render-app.

it's usually used inside Respo.

purify-element

This function flattens :data-events in the virtual DOM tree:

purify-element element

Event handlers in the virtual DOM tree can not be stringified. purify-element will turn the functions into a true.

mute-element

This function removes events from a virtual DOM tree:

mute-element element

When server side rendering is used, the first screen does not respond to events. That can be seen as rendered with a virtual DOM with no events. So mute-element is used to simulate such a virtual DOM tree.

find-element-diffs

Find diffs of virtual DOMs.

apply-dom-changes

Apply patches on the real DOM.

activate-instance

Function for initializing app:

  • setup event listeners the mount point element
  • render and mount element

patch-instance

Update based on virtual DOM.

Discuss

Perhaps I'm the only one to use Respo so far. But if you got troubles, here are some places you can post questions to:

Or you may reach me on Twitter .