Framework: using resize() with view()

This came up as a support question in our Community Slack, so I thought I’d share the solution here as well.

resize() cannot currently be combined with view() because it wraps the element in a div. Additionally, the element gets replaced which causes prior selections to get lost.

The workaround is to pass value through and to reassign it whenever the element is replaced.

~~~js
function resizeInput(...args) {
  const div = resize(...args);
  new MutationObserver((events => {
    const removed = events.find(d => d.removedNodes?.length)?.removedNodes;
    const added = events.find(d => d.addedNodes?.length)?.addedNodes;
    if(removed && added) added[0].value = removed[0].value;
  })).observe(div, {childList: true});

  return Object.defineProperty(div, "value", {
    get() { return this.firstElementChild?.value },
    set(v) {
      const n = this.firstElementChild;
      if(n) n.value = v;
    }
  });
}
~~~

~~~js
const selected = view(resizeInput(width => Plot.dot(penguins, {
  x: "culmen_length_mm",
  y: "culmen_depth_mm",
  tip: true
}).plot({width})));
~~~

~~~js echo
display(selected)
~~~

1 Like

Could this be fixed by modifying view() — or display() — in the code base?

Here is (in my opinion) a simpler approach, in particular one that avoids MutationObserver.

The core problem here is that Generators.input (which is used by view) expects the target element to expose an element.value property. The input event bubbles, so you can listen to the resizing <div> just fine — but you have to refer to event.target.value instead of element.value to report the new value of the input.

Here is a substitute for Generators.input that works with resize:

function valueof(element, initialValue = null) {
  return Generators.observe((change) => {
    const input = (event) => change(event.target.value);
    change(initialValue);
    element.addEventListener("input", input);
    return () => element.removeEventListener("input", input);
  });
}

Now you can say:

const penguinChart = resize((width) => Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm", tip: true}).plot({width}));
const penguin = valueof(penguinChart);

And then you can display your penguinChart wherever you want.

1 Like

@mbostock MutationObserver is used to transfer the value from the old element to the new element, not to capture it.

The code had a bug though (which I just fixed), and in hindsight Plot isn’t a good example because it doesn’t implement a setter for value.

I don’t follow your point. My point was that MutationObserver is a complicated and slow solution and that it’s better to change the generator definition (i.e., replace Generators.input) than try to change an element to conform to what view (and Generators.input) expects.

My point is

which affects the UI state of the input itself.

I don’t see how MutationObserver fixes this in general. The problem there is that the render function passed to resize is returning new elements. It’s convenient to just render new elements on resize (saying using Plot.plot) but if you want to more generally preserve internal element state across resize, you’ll need to do an incremental update instead of generating new DOM. Which you can do, say, using D3 or dom-diffing.

You can restore the current value in the case of Observable Inputs by assigning to element.value, but that’s not true in general. If you want this I think either you want incremental updates on resize, or you want to pass the current value into the render function so that the re-render can respect the current value (but even this won’t preserve other internal state such as text selection and focus).