This example uses the streams package for data model synchronization. It implements a TODO-MVC app using the VDOM reconciler.
The app.js
file can be bundled (into app.dist.js
) by running yarn stream
.
A simple static http server for the TODO-MVC can be started using yarn stream_server
.
The TODO-MVC app can then be accessed via http://localhost:8081
app.js
contains the main app entrypoint and the global event handlers.- render.js has all the core renderers. Each render is a simple pure
function that takes its data model and a state model. The state model
allows modeling UI state pretty much the same way as shared app
state. In fact, in
app,js
, state is created as a stream (though unlikedata
, thestate
is never synchronized between clients). - ui.js has a bunch of low-level UI primitives that would be present
in most high-level UI tool kits. Of particular interest is that most
form elements have
two-way bindings
where DOM events have sufficient information for thehandle
call to apply them. Some events (like clearing all completed todos) is harder to do and so these are handled explicitly by theApp
class.
The renderers generally accept two streams args:
- data holds the data relevant to the renderer (such as a todo item or the list of todo items). This is persisted on the server.
- state holds the session state local to the browser. This is setup in app.js.
An example renderer is how a single todo
item is rendered:
function todo(id, data, state) {
const deleteAction = ui.action.replace(data.deleted, true);
const c = data.completed.valueOf() ? "completed" : "";
return ui.withClass(c, {
tag: "li",
key: id,
props: {},
contents: [
{
tag: "div",
props: { class: "view" },
contents: [
ui.withClass("toggle", ui.stream.checkbox(data.completed)),
ui.stream.label(data.description),
ui.withClass("destroy", ui.stream.button("", deleteAction))
]
},
ui.withClass("edit", ui.stream.text(data.description))
]
});
}
The basic low-level UI primitives are implemented in ui.js. The text input is a good example which illustrates how mutations work:
function text(s) {
const events = { keyup: replace(s) };
const props = { type: "text", value: s.valueOf() || "" };
return { tag: "input", props, events, contents: {} };
}
The events
property is wired with keyup
event handlers. These map
to the replace(s)
action which returns a hash
{replace: s.ref()}
. The ref()
call is implemented by the
streams package -- this returns
the full path to the stream being used.
In the todo()
renderer example, data.desccription
is passed to
ui.stream.text
and so s.ref()
would be ['todos', id, 'description']
in this case (because data
is itself
root.todos.get(id)
).
The actual events handler is setup in
app.js
(handleEvent function) which routes to the handle
method in
ui.js
which then resolves the path into the correct stream object and
updates it accordingly.
The state
streams are a bit different in that they are rooted at a
different object. So,
app.js
sets up the state
stream to have a initial prefix of ['state']
which the event handler then uses to route to the right stream.
TODO: This can be vastly improved by using the `object` function
from the streams package.
A set of standard actions are implemented in ui.js:
- replacing a stream with the value from the current form element:
replace(s)
- replacing a stream with a fixed value:
replace(s, value)
- replacing a strean with a fixed hash but with one or more properties
whose value is from another stream:
repalce(s, {xyz: anotherStream})
. This is interesting because theanotherStream
itself can be live-bound to anotherinput
element. It can also be used to ensure that the replace value forxyz
is the current value at the time of mutation. The sameref
based trick is used here to make this happen. - multiple actions can be just grouped together to form an array.
- appending: all collections are just maps, so appending is just a
matter of using a new unique ID for the collection. In the TODO-MVC
app, this is done by using
uuidv4()
as the unique key generator.
Custom actions are not fully fleshed out. It is common for individual
renderers to have custom actions that are local to it. The current
solution in the example is to set up the action to be a unique string
(such as clear_completed
) and then have the action be implemented
globally by App
(i..e via the handleEvent
method in
app.js).
A better model would be a custom-action registration mechanism in ui.js.
The example here uses URL hashes to route filters (#/active') but to avoid polluting the renderers with this knowledge, the main
Appclass just copies the URL hash into
state.filter. Mutating the hash is done via simple
` tags and the main App
reacts to this and updates the state accordingly.
This trick of mutating the hashes vai <a>
links is awkward and does
not work so well with URL path elements. A better strategy is a
two-way binding between state.filter
and the URL hash: the renderer
just updates the state.filter
just like any other stream mutations
(via button + action replace) with the main app just reacting to that
and silently updating the URL (so that a refresh would work
correclty).