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">
<table>
  <tr>
    <td>${["r1", Range([0, 10])]}</td>
  </tr>
  <tr>
    <td>${["r2", Range([0, 3])]}</td>
  </tr>
  <tr>
    <td>${[
      "text",
      Text({
        label: "Enter some text"
      })
    ]}</td>
  </tr>
</table>
<img width="200"src="https://media.giphy.com/media/2vobTwCkFg88ZUnilt/giphy-downsized.gif"></img>
</div>
`
8 Likes

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(
      Inputs.text({
        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?")}`);
  }
})
2 Likes

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!

Some better documentation was requested… here it is!

it includes a substantial port of a famous React example by Micheal Jackson (UNPKG, Redux). I think the observablehq view way is better! Hopefully this gives people a few ideas on how they can reuse UI components better.

3 Likes

Finally got around to the last missing piece for the view-literal, dynamic object assignments.

If you add a viewBuilder function as the third arg to a view-literal instruction:

    viewof demo = view`<div>['...', {}, (text) => Inputs.text({value: text})]`

it will use that builder to create new UI elements if that value is assigned

demo.value = {a : "my text"}

And it will prune the UI if you assing {}. So you can do backwritable dynamic keyed collections now.

I have some regrets. It would be nice if it did it could use the builder with normal property assignment like demo.value.a = “my text”, similarly for dynamic lists it be nice if I could just use the array mutation operators like v.array.push() and it would build the UI. Maybe dynamic proxies? I think I need to split the everything-view into separate scalar, array and object variants. Now I see the big picture it’s really “just an instrumented JSON”.

Anyway, I write lots of tests, the code has becomes massive and convoluted as I have added features, but the tests are to help define what the refactored replacement should do. Its broadly feature complete in that there is a way of achieving all the obvious things you would want to do, but also its incomplete in the sense there are gaps in the API so it’s not quite as easy and natural as working with native arrays and objects.

2 Likes

Very inspiring. I reminded me that I actually wanted to write dynamic tax calculator with all the sliders and connections showing what depends from what. One day I may do this… if I ever understand their maths. :smiley:

2 Likes

I have had a couple of requests along the lines of “how to dynamically change the options in a select” for complex UIs.

This is surprisingly hard because the options in Inputs.select are not part of the value, so you can’t back write them dynamically! Variations of this issue bite me constantly when building complex UIs. How I want to use a UI component, and the degrees of freedom offered in the signature are often not in alignment.

SO I FIXED IT

Hopefully this makes everybodies great UI libraries stretch a little further…

3 Likes

BTW I have found juice very useful in lots of places. It’s almost a cheat code for reactivity as you can just artificially create a builder and then juice it into a reactive component without doing any wiring.

juice((args) => view`...`, {width: "[0].width"})

Even with view I still find building complex UIs too much work. The problem is getting things to work on mobile and desktop is tricky with CSS, and view only exposes HTML with gets pretty deep with responsive layouts.

So I long for the days of Visual Basic Form Builder, I created something simple like that. A simple grid, on panels that rearrange. 3 columns for desktop, less on smaller screens. Now I can just lay out components in absolute coordinates, but the panels rearrange to make it work. So much simpler!

It’s all reactive too. So it can all be tweaked in real time too via external logic.

1 Like

As I try to create large UIs, I have found view hits a performance issue because it generally removes its DOM and replaces it even for minor operations like adding an element to an array. It seems arrays are super useful for HTML, naturally representing table rows or columns or list elements.

view has needed a refactor a while, so I have taken the opportunity to refactor out “array” handling in view, and apply a performance optimization.

If you do an in-place mutation of an array, it will mirror that in-place mutation in the DOM. So

viewof myArrayView.value.push(4)

or

myArrayView.push(4)

will instantiate a DOM node (using the construction time builder arg) to hold the 4 and append it to the DOM elements. I have support for push,pop,shift,unshift,splice

After finishing I realised for symmetry you should also be able to

viewof myArrayView.push(Inputs.number({value: 4}))

but I have not generalized that far. Also I need to do a similar thing for dynamic objects. This is all too much work right this second, BUT, at least I have finally managed to refactor view a bit which should make future refactors a little bit easier.

I have tried to preserve backwards compatibility, the unit test coverage is expanding, but let me know if something has broken.

1 Like