Skip to content

Documentation

David Hofmann edited this page Sep 16, 2022 · 11 revisions

Table of contents


Overview

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 πŸ”‹πŸ”‹πŸ”‹


Core

Signals

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

Stores

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

Views

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 predefined vdom.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.

Connect

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

Addons

Effects

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

Router

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 a routing.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))))

Element macros

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)
Clone this wiki locally