-
Notifications
You must be signed in to change notification settings - Fork 0
Documentation
Welcome to the documentation.
Silux implements the Flux architecture with a centralized store that incorporates the dispatcher. This means that signals are dispatched to the store directly through a store's dispatch
method, invoking a state transition via an update
function (a reducer: (state, signal, dispatch) -> state
). Whenever the state changes, the store notifies views which have subscribed via it's subscribe
method about the changed state.
This documentation gives an in depth overview about the way silux is build and intended to be used. Don't worry, you usually don't need to work with all things described directly. The main pattern when using silux is:
- Define a (set of) signal(s)
- Define a (combination of) update function(s)
- Define a (set of) view function(s) that create(s) a virtual DOM tree
To setup an application and run it:
- Create a store with an initial state and the (combined) update function(s)
- Connect the store, the view function and a DOM node
- Run the app
In pseudo code:
; # = placeholder
(defsum signal ((:type-a x)
(:type-b y z)))
(defun update (state sign dispatch)
#NEW-STATE#)
(defun view ({state} dispatch)
#VDOM#)
(defvar *run* (silux-connect
(silux-store #INITIAL-STATE# update)
(hash :el #HTMLElement#
:view view)))
(*run*)
All essentials to accomplish the tasks described above are included πππ
Signals are used to pass information into a store about in which ways state should be transformed. The signal does not change the state itself, it just communicates an intent to the store which then reacts to that intent (or it even might not react). Updating the state and then broadcasting a changed state is the task of the store.
You are free to decide what you use as signals in your application. In a very simple case, you might just use strings that describe what action to perform, for more complex problems you might want to use a more structured solution like (hash)
structures or tagged sum types via (defsum)
.
Main functions:
none
The stores are the main building block in silux, acting as subscribable state (may be any value) containers that can take incomings signals via their dispatch
method as well as publishing their contained state to subscriber functions added via subscribe
. Subscriber functions receive a (hash :state state)
like structure where the :state
field contains whatever value is stored. It is common that this object is extended via plugins
.
Main functions:
silux-store :: (a, Update, List Plugin) -> Store a Update
where
Plugin = Store a Update -> Store' a Update
Update = (a, Signal, Dispatch) -> a'
Dispatch = Signal -> ()
Signal = any
a = any
silux-fold-updates :: (&rest Update) -> Update
where
Update = (a, Signal, Dispatch) -> a'
Dispatch = Signal -> ()
Signal = any
a = any
Example:
;; A simple counter update function. Note the dispatch
;; parameter (unused), which allows to dispatch signals
;; asynchronously to the store
(defun update (state signal dispatch)
(match-sum signal ((:incr ()
(+ state 1)))))
;; Stands in for a view function that produces a virtual
;; DOM tree
(defun log-state ({state})
(.log console state))
(defvar *counter* (silux-store 0 update))
(.get-state *counter*) ; => 0
(.dispatch *counter* (signal.incr)) ; => *counter*
(.get-state *counter*) ; => 1
(.subscribe *counter* log-state) ; => *counter*
(.dispatch *counter* (signal.incr)) ; => *counter*, logs 2
(.get-state *counter*) ; => 2
(.notify *counter*) ; => *counter*, logs 2
View functions in Silux are pure functions that take a store's StateCollection
and an emit
function and produce a virtual DOM tree as a result.
Main functions:
h :: (Tagname, Props, &rest String|VDOM) -> VDOM
where
Tagname = String
Props = Snabbdom Properties
VDOM = Snabbdom VNode
Example:
(defun view ({state} emit)
(h "div" (hash :props (hash :id "app"
:class-name "container")
:dataset (hash :foo "bar")
:on (hash :click (#>
(.log console
"clicked!"))))
(h "h1" (hash) "Hello, Silux!")))
Macros
Silux ships a predefinedvdom.sibilant
macro file that includes macro definitions of most HTML elements as well as a general utility macro for elements that are missing. If you are a Sibilisp user, you can include the macro file and get a JSX like experience when writing your view code(s) without having to setup JSX processing in your build chain. And since Sibilisp supports the ES2015 module system, you are able to compile your JSX-like code directly into JavaScript that can be executed by a browser. π€©
β οΈ Before V0.8.0, the virtual DOM macros where available via(include "silux/macros")
, which is now deprecated.Support for them will be dropped in the future. If you use them, please switch to(include "silux/vdom")
.
(use "silux" h) ; the vdom macros require the presence of the "h" function
(include "silux/vdom")
(import-namespace silux)
(defun view ({state} emit)
;; Look ma', no JSX
($div (:id "app"
:class "container"
:data-foo "bar"
:onclick (#> (.log console "clicked!")))
($h1 () "Hello, Silux!")))
Yep, this creates the same structure like the snippet above that uses (h)
You will find a list of supported elements at the end of this documentation.
To wire everything up, use silux-connect
. It takes a store and one to many (hash :el :view)
structures. The :el
field has to point to an existing HTMLElement
and the :view
field has to reference a virtual DOM creating function (read: a view
function). silux-connect
then wires everything up so that state changes get propagated to all connected views.
Main functions:
silux-connect :: (Store a Update, &rest ViewPairing) ->
(&optional Signal) ->
nil
where
ViewPairing = (hash :el HTMLElement :view ViewFunction)
ViewFunction = ((hash :state any), Dispatch) -> VDOM
Update = ((a, Signal, Dispatch) -> a')
Dispatch = (Signal -> ())
Signal = any
a = any
Example:
(defun update (state signal))
(defun view ({state} emit))
(defvar *run*
(silux-connect
(silux-store (nil) update)
(hash :el (.query-selector document "#app")
:view view)))
(*run*)
For certain tasks you might want to run a side effect on certain incoming signals and later dispatch follow-up signals when data that was earlier missing is available. One example for that scenario is an XMLHttpRequest to a server. However, you also want to have a more concise update
function by not calling the dispatch
function inside of it.
Silux has you covered: By utilizing the with-effects
function when returning a new state, you are free to run various side effects after the DOM tree has been updated. This includes things like the above mentioned HTTP communication, but also stuff like logging or persisting information in - for example - the LocalStorage
.
Main functions:
with-effects :: (a, &rest EffectFactory) -> EffectfulStoreState
where
EffectFactory = (&optional &rest Config) -> ((Dispatch, a) -> any)
Dispatch = Signal -> ()
Config = any
a = any
Example:
;; a simple effect function that logs
;; the next state to the console. however,
;; notice the existance of dispatch, which
;; allows to dispatch actions back into the store.
;;
;; note that, although available, you should not
;; modify the state reference inside of an
;; effect (doing so leads to unspecified behaviour)
(defun log-fx (topic)
(#(dispatch state)
(.log console topic state)))
(defun update (state signal)
(with-effects (+ state 1)
(log-fx "changed state")))
You need routing in your application? No problem, Silux has a build-in routing system that works with the HTML5 history API as well as.
Main functions:
silux-router
, silux-router-plugin
, go-route
Example:
(use "silux"
silux-store
silux-connect
silux-router
silux-router-plugin
go-route
h)
(include "silux/vdom")
(import-namespace silux)
(deftype signal (new-title))
(defun update (_ sign)
(getf sign 'new-title))
;;; The view function below serves for both routes and we need
;;; the **pages constant mapping to keep it generic
(defconstant **pages (hash :A (hash :url "/b" :title "B")
:B (hash :url "/" :title "A")))
(defun view ({state route} emit)
($div (:id "app" :class "page")
($h1 (:class "h1")
(+ "Welcome to page " state "!"))
($p ()
(+ "You are on path: " (getf route 'path)))
(with-fields (getf **pages state) (url title)
($a (:href url
:onclick (#(event)
(.prevent-default event)
(emit
(go-route url (signal title))))
:class "my-link"
:target "_blank")
(+ "Go to page " title)))))
(call
(silux-connect
(silux-store "A" update (list (silux-router-plugin)))
(hash :el (.query-selector document "#app")
:view (silux-router (hash :"/" view
:"/b" view)))))
Macros
Heads up! Silux contains arouting.sibilant
macro file that includes macros for woring with the routing stuff. To get a first impression, have a look at the example below which re-creates the above one.
(use "silux"
silux-store
silux-connect
silux-router-plugin
silux-router ; like the vdom macros, the routing macros need "their" functions
go-route ; in scope (meaning: silux-router, go-route & h)
h)
(include "silux/vdom")
(include "silux/routing")
(import-namespace silux)
(deftype signal (new-title))
(defun update (_ sign)
(getf sign 'new-title))
(defconstant **pages (hash :A (hash :url "/b" :title "B")
:B (hash :url "/" :title "A")))
(defun view ({state route} emit)
($div (:id "app" :class "page")
($h1 (:class "h1")
(+ "Welcome to page " state "!"))
($p ()
(+ "You are on path: " (getf route 'path)))
(with-fields (getf **pages state) (url title)
($route-link (:&url ; macro $route-link
:emitter emit ; optional, omit for normal <a href="">...</a>
:signal (#> (signal title)) ; optional, omit for just chaning the route
:class "my-link" ; optional
:target "_blank") ; optional
(+ "Go to page " title)))))
(call
(silux-connect
(silux-store "A" update (list (silux-router-plugin)))
(hash :el (.query-selector document "#app")
:view ($router "/" view ; macro $router
"/b" view))))
This table lists all macros for element creation inside silux/vdom
.
Element | Macro | Signature |
---|---|---|
<main> |
β | ($main (&rest :prop value) &rest children) |
<section> |
β | ($section (&rest :prop value) &rest children) |
<aside> |
β | ($aside (&rest :prop value) &rest children) |
<article> |
β | ($article (&rest :prop value) &rest children) |
<header> |
β | ($header (&rest :prop value) &rest children) |
<footer> |
β | ($footer (&rest :prop value) &rest children) |
<nav> |
β | ($nav (&rest :prop value) &rest children) |
<menu> |
β | ($menu (&rest :prop value) &rest children) |
<ul> |
β | ($ul (&rest :prop value) &rest children) |
<ol> |
β | ($ol (&rest :prop value) &rest children) |
<li> |
β | ($li (&rest :prop value) &rest children) |
<div> |
β | ($div (&rest :prop value) &rest children) |
<table> |
β | ($table (&rest :prop value) &rest children) |
<thead> |
β | ($thead (&rest :prop value) &rest children) |
<tbody> |
β | ($tbody (&rest :prop value) &rest children) |
<tr> |
β | ($tr (&rest :prop value) &rest children) |
<th> |
β | ($th (&rest :prop value) &rest children) |
<td> |
β | ($td (&rest :prop value) &rest children) |
<h1> |
β | ($h1 (&rest :prop value) &rest children) |
<h2> |
β | ($h2 (&rest :prop value) &rest children) |
<h3> |
β | ($h3 (&rest :prop value) &rest children) |
<h4> |
β | ($h4 (&rest :prop value) &rest children) |
<h5> |
β | ($h5 (&rest :prop value) &rest children) |
<h6> |
β | ($h6 (&rest :prop value) &rest children) |
<p> |
β | ($p (&rest :prop value) &rest children) |
<span> |
β | ($span (&rest :prop value) &rest children) |
<strong> |
β | ($strong &rest children) |
<em> |
β | ($em &rest children) |
<sup> |
β | ($sup &rest children) |
<sub> |
β | ($sub &rest children) |
<br> |
β | ($br) |
<hr> |
β | ($h1 (&rest :prop value)) |
<form> |
β | ($form (&rest :prop value) &rest children) |
<button> |
β | ($button (&rest :prop value) &rest children) |
<label> |
β | ($label (&rest :prop value) &rest children) |
<input> |
β | ($input (&rest :prop value)) |
<output> |
β | ($output (&rest :prop value) &rest children) |
<select> |
β | ($select (&rest :prop value) &rest children) |
<option> |
β | ($option (&rest :prop value) &rest children) |
<textarea> |
β | ($textarea (&rest :prop value) &rest children) |
<fieldset> |
β | ($fieldset (&rest :prop value) &rest children) |
<legend> |
β | ($legend (&rest :prop value) &rest children) |
<img> |
β | ($img (&rest :prop value)) |
<picture> |
β | ($picture (&rest :prop value) &rest children) |
<figure> |
β | ($figure (&rest :prop value) &rest children) |
<figcaption> |
β | ($figcaption (&rest :prop value) &rest children) |
<canvas> |
β | ($canvas (&rest :prop value) &rest children) |
<video> |
β | ($video (&rest :prop value) &rest children) |
<audio> |
β | ($audio (&rest :prop value) &rest children) |
<source> |
β | ($source (&rest :prop value) &rest children) |
<track> |
β | ($track (&rest :prop value) &rest children) |
<address> |
β | ($address (&rest :prop value) &rest children) |
<blockquote> |
β | ($blockquote (&rest :prop value) &rest children) |
<dl> |
β | ($dl (&rest :prop value) &rest children) |
<dt> |
β | ($dt (&rest :prop value) &rest children) |
<dd> |
β | ($dd (&rest :prop value) &rest children) |
<dfn> |
β | ($dfn (&rest :prop value) &rest children) |
<pre> |
β | ($pre (&rest :prop value) &rest children) |
<code> |
β | ($code (&rest :prop value) &rest children) |
<abbr> |
β | ($abbr (&rest :prop value) &rest children) |
<bdi> |
β | ($bdi (&rest :prop value) &rest children) |
<bdo> |
β | ($bdo (&rest :prop value) &rest children) |
<cite> |
β | ($cite (&rest :prop value) &rest children) |
<data> |
β | ($data (&rest :prop value) &rest children) |
<rp> |
β | ($rp &rest children) |
<rt> |
β | ($rt &rest children) |
<kbd> |
β | ($kbd &rest children) |
<mark> |
β | ($mark &rest children) |
<ruby> |
β | ($ruby &rest children) |
<small> |
β | ($small &rest children) |
<b> |
β | ($b (&rest :prop value) &rest children) |
<i> |
β | ($i (&rest :prop value) &rest children) |
<s> |
β | ($s (&rest :prop value) &rest children) |
<u> |
β | ($u (&rest :prop value) &rest children) |
<q> |
β | ($q (&rest :prop value) &rest children) |
<samp> |
β | ($samp &rest children) |
<time> |
β | ($time (&rest :prop value) &rest children) |
<var> |
β | ($var &rest children) |
<wbr> |
β | ($wbr) |
<map> |
π« | use ($h "map" (&rest :prop value) &rest children)
|
<area> |
π« | use ($h "area" (&rest :prop value) &rest children)
|