Getting Inputs.bind() to work on a custom component?

For the last few days I’ve been trying to get this ChoicesJS component work nicely with Inputs.bind(). But no matter what I try I can’t get it to work. I’m sure it’s possible but it’s just my limited knowledge here that’s stopping progress. I’ve created this notebook to play around in.

Any help in the right direction is much appreciated!

It looks like your widget is missing a setter. A useful pattern for custom widgets (here demonstrating a fictional widget that exposes its value via functions) is:

return Object.defineProperty(htl.html`<div>${myWidget}`, "value", {
  get: () => myWidget.getValue(),
  set: newValue => myWidget.setValue(newValue)
});

Note that we don’t dispatch an input event when setting the value for two reasons:

  1. it matches the behavior of native input elements
  2. it could trigger an update loop

Please also avoid any code that modifies the DOM directly, like document.body.appendChild. The only reason this seems to work in your code is that you include the element in your container, which effectively moves it back from the body into your cell output.


That said, I wonder if you might want to pick a different library than Choices. Two of its characteristics make it seem like a bad fit for Observable:

  1. it requires the select element to be present in the DOM, so that you have to jump through additional hoops (like requestAnimationFrame) to initialize it after your cell has already returned. (edit: this turned out to be wrong)
  2. It uses an absolute positioned dropdown. Any content inside the notebook sandbox is covered by pinned editors, so chances are high that the dropdown might get hidden behind an open editor.

Thanks for helping out @mootari ! I’m noticing that I having a hard time grasping some of the concepts you share :smile:. Really a complete newbie when it comes to creating custom widgets. I haven’t created this ChoicesJS widget myself but started out with a fork from a different notebook.

Do you maybe have a working example from a different widget that I could use as inspiration?

I have noticed the behaviour that you describe where the dropbox disappears underneath pinned editors. I’m okay paying that price in return for the features that ChoicesJS offers. Right now I’m using the widget mostly embedded on a page so the absolute positioning isn’t really an issue there.

A while back I wrote this introduction to viewof which doesn’t cover setters though.

Maybe you could list a few points that you find confusing or that you’re stuck on, and I will try to answer them individually?

Thanks @mootari that’s a helpful article and definitely helps to better understand the inner workings of viewof.

Getting closer but I’m still not quite there!

I’ve updated my notebook and to my best knowledge, added a setter and getter functions inside the widget.

There are 2 things I’m now stuck on:

  1. Updating the values of the dropdown only works for adding new values. Removing values isn’t working? Could it be that this is related to the fact the the two dropdowns are binded?

  2. I’m not able to update the actual value of container.value as that gets me into an infinite loop. So the value of selection2 doesn’t return the correct value until I manually update that widget.

UPDATE:
The second problem seems to be fixed by adding container.dispatchEvent(new CustomEvent("input", {bubbles: true})); to the setter. Now just stuck on the weird first problem.

Great to see how quickly you’re making progress!

I’ve created an example for native selects here (still needs more explanations / documentation):

and a prototype of a Choices widget that matches the API of the Inputs widgets:

Will add more details once I find the time, but maybe this will already help you get going.

1 Like

This is insane @mootari :slight_smile: Still trying to grasp what’s happening in your example line by line. Appreciate you took the time to put this together. Hope it will help many others in the community.

2 Likes

Let me know if there’s anything in particular that you’d like explained in more detail!

@mootari I’m wondering what the recommended approach is when you want to bind more than two widgets together. I know the Inputs.bind() returns the returns widget but what I want to bind 5 widgets. Is it better to first create the individual viewof instances and then do multiple bind calls or should you just use the output of bind()? Hope I’m making sense.

Sidenote, having widget.lastElementChild.style.cssText += "overflow: hidden"; enabled in one of my other notebooks hides the dropdown with the list of options when clicked. Any pointers on how to debug this?

I would recommend to create only one viewof cell and have that be your primary input (and the only one with a default value, if any):

viewof text = Inputs.text({value: "default value"}) // first cell
Inputs.bind(Inputs.text(), viewof text) // second cell
Inputs.bind(Inputs.text(), viewof text) // third cell

While you can link two viewof cells together via Inputs.bind() in a third cell, I would generally advise against that. Avoid side effects whenever you can: the cell that displays the secondary input should also initiate the binding.

The dropdown is positioned absolute and thus removed from the layout flow. It has no dimensions that could stretch the parent element.

If you take a closer look at my implementation you may notice that I chose to position the dropdown relatively, so that it expands the cell and cannot be covered by editors.

Debugging is tricky, as the display state is controlled via JavaScript. You could add a CSS rule to always display it:

.choices__list.choices__list--dropdown { display: block }

When I test this approach, I run into the problem that when cell #3 is updated, the output of cell #2 doesn’t change. The value does change in cell #2 dropdown but the values in the resulting array aren’t updated. Here’s an example based on your notebook.

Yes, as mentioned you should only obtain the value from the first widget, which is what I demonstrated in my example above. There isn’t really a point in linking multiple viewof’s, since the widgets should all share the same value.

Inputs.bind won’t dispatch an input event on the widget when setting the widget value, and therefore the cell value won’t register as updated. Consider any additional inputs to be an “extension” (or copy) of your first, not actual new cell values.

@mootari Hmmm, but what if you have a really long report and want to have a dropdown (lets say a country filter) in multiple places along the report. Changing the dropdown in one place of the report should update the other instances as well, right? Is that doable? I’m trying to understand the dynamics here.

That is precisely what Inputs.bind() is for. Your report cells only reference your primary widget cell. The other widgets only act as “remote controls” for your primary widget. :slight_smile:

But if you take a look over here you’ll see that it’s not working, or at least not how I would expect :slight_smile: Change the value of dropdown3 and look at the value array or dropdown2. That array isn’t being updated but the value inside the dropdown is.

choicesSelection is your primary input, and the only one that you should reference in other cells. dropdown2 and dropdown3 don’t need to be named.

@mootari Got it! That needed to click in my mind. Thanks for your patience :slight_smile:

One more question. If you want to embed the different dropdowns on an external page then naming them would make sense right? Or would you be able to embed the single dropdown instance multiple times?

You just need to make sure that you’re reading values from your “main” dropdown.

Yes, although you don’t need the viewof prefix in this case. The name will then just reference the DOM element.

No worries! Would you be willing to share if there’s a certain perspective or assumption that may have misled you? Building an intuition for these concepts can be tricky, and I’d love to learn what I should look out for. :pray:

Works like a charm!

Sure, what clicked was the fact that you just have one viewof instance and all the subsequent widgets are just a reference to that. In my head I had multiple viewof instances that all referenced (and updated) each other. So in my charts, I was also using a different viewof instance that corresponded to a specific section of a notebook. But now I only have one “main” dropdown and all my charts reference that. Of course a much more elegant solution.

To be honest, I don’t think I came across an example in the documentation related to Inpts.bind() that uses this approach. Maybe it would be good to point out when you’d use just one instance and when you’d use multiple instances.

1 Like

That’s great feedback, thank you!