domorphic
v0.0.3
Published
pure js DOM templating
Downloads
2
Maintainers
Readme
domorphic
is a plain javascript program to interact with the DOM, in a functional, reactive and keep-it-simple philosophy.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/dom.min.js"></script>
Inspired by the power of d3 and the beauty of elm, this library attempts to breed their many respective qualities, to make shaping DOM interfaces within js enjoyable, smooth and pure.
Foreword
There are two layers to this library:
the first one is really just convenience for templating: pure js code which outputs DOM subtrees, regardless of anything else, so that you may very well be content with it.
the second one, much more subtle, is about designing stateful and reactive applications with pure monadic code.
Whether you've ever heard about monads or not, they're intuitive and powerful, though there's no need to worship their metaphysical nature to use the package :) For more documentation and examples, visit the project's website !
Usage
Everything happens in the polymorphic type a -> Node
of functions returning DOM nodes,
which in scientific terms,
is also called the category of types above Node
.
What?!
Above the DOM. A domorphic instance is just a function returning a DOM node. That's it!
let node = dom('h1').html('Hello World:!')();
At first glance, the library hence only helps you
conveniently parametrise functions returning DOM nodes.
And you may very well do document.body.appendChild
or whatever you like with them.
Syntax. All view-related code is meant to be as light and readable as any other templating language: except it's javascript!
The dom constructor parses arguments looking for the following pattern:
dom('tag#id.class', ?{...attrs}, m ?-> [...branches])
Each branch may be given either as a dom instance, or as an array of arguments following the same pattern, so that you may nest arrays just as you would nest html tags.
Attributes. All node attributes are interpreted either as values or as data dependent functions.
You may supply them by d3-like chainable accessors:
let a = dom('a')
.html("internet")
.attr('href', m => m.href)
.on('hover', () => alert("wooo"))
although the dom constructor also interprets the equivalent syntax:
let a = dom('a', {
html: "internet",
href: m => m.href,
onhover: () => alert("wooo")),
});
Branches.
Nodes are essentially trees of DOM elements, i.e.
Node = (Element, [Node])
.
The branching attribute itself may be given
in a -> [Node]
, as a node array returning function:
let m = ['cats', 'are', 'cute'],
// p : Str -> Node
let p = dom('p')
.html(m => m);
// div : [Str] -> Node
let div = dom('div')
.branch(m => m.map(p));
document.body.appendChild(div(m));
and the above produces the same output as:
// div : () -> Node
let div = dom('div', [
['p', {html: "cats"}],
['p', {html: "are"}],
['p', {html: "cute"}]
]);
document.body.appendChild(div());
Functors
In the category of types, a
functor T
:
- assigns to every type
a
a typeT a
, - transforms any map
a -> b
to a mapT a -> T b
.
Because dom instances are pure functions, it is perfectly safe to pipe them into functorial transformations.
Pullbacks.
Given a function a -> b
, precompose a b -> Node
instance
to get an a -> Node
map:
dom.pull : (a -> b) -> (b -> Node) -> (a -> Node)
You're looking at
the contravariant hom-functor
Hom(-, Node)
in the cartesian category of types :)
Arrays.
Functions of type a -> Node
are naturally transformed to [a] -> [Node]
maps:
dom.map : (a -> Node) -> [a] -> [Node]
As far as dom instances are pure functions, this
is rigorously equivalent to calling Array.map
.
Using dom.map
will be mostly useful to associate
index-specific DOM actions in the IO monad.
Records.
Similarly, we map a -> Node
to a type of functions on records {a} -> {Node}
:
dom.rmap : (a -> Node) -> {a} -> {Node}
This is not yet implemented, but there's little more to it than its array counterpart.
Monads
The main originality of domorphic is the monadic approach it takes to describe interactions between an internal state and the DOM state.
KISS: no automatic diff refreshes -- only handmade, pure pipelines.
IO. The IO monad describes input/output operations with the DOM.
IO(e) : IO operations, eventually triggering an event of type e.
State. The State monad describes stateful computations.
St(s, a) = s -> (a, s) : Computations with state in s, return value in a.
Updates.
The State and IO monads yield a composed monadic type St(s, IO(e))
.
Upon an event of type e
, this monad describes
how to update the internal state and
which IO operations should take place:
update : e -> St(s, IO(e)) : Upon event, update state and trigger IO actions.
Binding then the update function to its return value just defines the main loop!
let main = (e0, s0) => {
let [io, s1] = update(e0).run(s0);
return io.bind(e1 => main(e1, s1));
}
let start = s0 => main('start', s0);