🏠 back to Observable

Compose UIs (spatially and temporally)

Make complex UIs (a.k.a. views) from simpler ones.

Wraps the hypertext literal with an extra special case for [key, DOM]. This allows you to design a custom UI using HTML, and tell the custom template how to bind sub-views to properties of the parent value. Simple but powerful.

viewof composite = view`<div style="display: flex">
    <td>${["r1", Range([0, 10])]}</td>
    <td>${["r2", Range([0, 3])]}</td>
        label: "Enter some text"
<img width="200"src="https://media.giphy.com/media/2vobTwCkFg88ZUnilt/giphy-downsized.gif"></img>

So the above is how to compose views spatially, but I have found the need to compose views temporally =>

Sequences of UIs are composed by yielding DOM elements. This is quite nice as you can carry state over the top and make programatic choices etc. E.g.:

viewof example1 = viewroutine(async function*() {
  let newName = undefined;
  while (true) {
    newName = yield* ask(
        label: "please enter the name of the thing to create",
        minlength: 1,
        value: newName,
        submit: true
    yield md`<mark>updating to ${newName}`; // Note we can remember newName
    await new Promise(r => setTimeout(r, 1000)); // Mock async action
    yield* ask(htl.html`${md`<mark>updated`} ${Inputs.button("Again?")}`);

the view literal got an upgrade today.

You can now use a spread key to create a simple wrapper of an existing view

viewof num = view<div><h1>My entitled control</h1>${['...', Inputs.range()]}

This spread also works for destructuring an object

viewof num = view${['...', { 'v1': Inputs.range(), 'v2': Inputs.text() }]}

The motivation for these semantics was creating tables where you need to wrap inner elements in <td>, and the row/column values is conveniently represented as a dictionaries.

I also documented the use of array collections.

1 Like

Just created a draggable file input that uses @mbostock’s cool FileAttachment’s prototype trick to output an FileAttachment .

What is noteworthy is how easy it is to use the view literal to wrap and transform other views.

It uses the spread operator on an Inputs.input(…)

   const output = Inputs.input() 
   view`... <!-- ['...', output] -->`
   output.value = ...

to create a custom UI over a very minimalistic value holder. In out case we don’t want the component to render so I also hide it in a comment, which is weird and I need a better way to do that but its ok for now.

We can then programatically assign the output in response to external events. In out case we listen to a HTML file input and wrap the file attachment with the the LocalFileAttachment stuff. Combined with the ability to add inline the whole thing can be expressed in a single cell.

1 Like

Here is an Inputs.table-like writable table Data Editor / Tom Larkworthy / Observable with drag and drop reordering.

To get that working array-of-views bindings for view literal is now mutable which is quite cool for expressivity.

The data editor offers a minimal splice API so adjacent cells can add/delete rows programmatically. This allows decoupling the “add item” UI cell from the data editor list view cell. I think this is a good way to break features across cells using back-writability.

I need this component for a commercial project. I tried to make it general though but I don’t have too much time for non-essential features, but I am keen to support it so Suggestions or forks are very welcome.

1 Like

I love that you’re keeping all of these announcements threaded (helps to follow along)! Thanks for all this shared work!