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?
- Minimal App https://github.com/Respo/minimal-respo
- Examples https://github.com/Respo/respo-examples
- Complicated editor in Respo https://github.com/Cirru/calcit-editor
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:
- Calcit-js http://calcit-lang.org
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:
- Minimal app https://github.com/Respo/minimal-respo
- Wanderlist https://github.com/Memkits/wanderlist
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?
beingtrue
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-ui
for base stylesrespo-value
for displaying valuesrespo-router
for routering(Respo not required)global-popup
for global popupsinflow-popup
for local popups(demo only)notifier
for showing notifications(Respo not required)
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
Namespace | Function |
---|---|
respo.core | defcomp |
div | |
<> | |
defeffect | |
create-element | |
render! | |
clear-cache! | |
realize-ssr! | |
list-> | |
>> | |
respo.comp.space | comp-space or =< |
respo.comp.inspect | comp-inspect |
respo.render.html | make-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.
Namespace | Function |
---|---|
respo.render.expand | render-app |
respo.util.format | purify-element |
mute-element | |
respo.util.list | map-val |
map-with-idx | |
respo.render.diff | find-element-diffs |
respo.render.patch | apply-dom-changes |
respo.controller.client | activate-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?
beingtrue
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:
- DOM diffing
- 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:
- Discussions https://github.com/Respo/respo.calcit/discussions .
Or you may reach me on Twitter .