🏠 back to Observable

How to return 'undefined' for an Observable input?

In Mike’s Conditional Form notebook, the form will return an undefined value before a radio button is clicked.

If I am reading the form’s code correctly, this bit is responsible—not deferring evaluation of the form until an input is provided:

if (value !== undefined) form.choose.value = value;
  form.value = Promise.resolve(disabled ? undefined : value); // Don’t defer evaluation.

Is there a quick and easy way to ‘append’ in this behavior to the radio input from Jeremy’s Inputs Bazaar?

Jeremy’s inputs seem to defer evaluation, but I’d like to write a function that interprets non-responses.

Here’s a quick notebook that shows the described behavior for each input:

And while I’m asking questions: Is there any way to ‘uncheck’ a radio button?

2 Likes

Yes! If null is good enough for you, you can get away with:

viewof r1 = Object.assign(radio({
  // options ...
}), {value: null})

But if you insist on undefined, then you need to wrap the value in a promise, like Mike did:

viewof r1 = Object.assign(radio({
  // options ...
}), {value: Promise.resolve(undefined)})

I’ll get into the details of the Why? in a follow-up comment.

Yes, but only programmatically. Simply set the input’s checked property to any falsy value. E.g., given a cell:

r = html`<input type=radio checked>`

… we can create a button that will uncheck the radio:

Object.assign(html`<button>Uncheck`, {
  onclick() { r.checked = null }
})
2 Likes

To understand why undefined isn’t good enough, we’ll create a simple test case, a notebook with a single viewof cell:

viewof hello = html`<div>`

Let’s take a look at the generated code:

  1. Enable link sharing.
  2. Open up the cell’s Embed code dialog.
  3. Look for the script URL after import define from and copy it.
  4. Open the script URL in your browser.

You should see something like this:

// https://observablehq.com/d/0123456789abcdef@1
export default function define(runtime, observer) {
  const main = runtime.module();
  main.variable(observer("viewof hello")).define("viewof hello", ["html"], function(html){return(
html`<div>`
)});
  main.variable(observer("hello")).define("hello", ["Generators", "viewof hello"], (G, _) => G.input(_));
  return main;
}

Observable wraps each cell inside a so-called Variable instance. Looking through the code, we see our hello cell defined. Actually, we see it defined twice:

  1. viewof hello: The “original” cell, containing our <div> DOM element,
  2. hello: This is the value of our viewof cell that gets passed to other cells.

In the second define() we can rename the arguments G and _ to end up with a cell that reads:

Generators.input(viewof hello)

If you had looked at Observable’s Standard Library before, then Generators.input() might look familiar. Its documentation even tells us:

Generators.input is used by Observable’s viewof operator to define the current value of a view […] it can be used to define a generator cell exposing the current value of an input […].

And right at the top of the function’s documentation, we find our answer:

[…] If the initial value of the input is not undefined, the returned generator’s first yielded value is a resolved promise with the initial value of the input .

By passing undefined, we end up with a Generator that won’t yield until the next input event.


Going back to Jeremy’s Inputs library, and more specifically radio(), we need to understand the following:

  1. radio() won’t blindly set value to whatever we pass in our widget configuration. Instead it will mark any of the radio inputs as checked if its value matches.
  2. radio() relies on input() for some of its functionality. Instead of settings the value directly, radio() passes a getValue() callback along.
  3. This getValue() callback will go through the widget’s radio inputs to look for the first one that’s checked, and return its value. If none is checked, it will return undefined.
  4. input() will gladly take whatever value the callback returns, and set it as the cell’s value.

So there you have it, a much too elaborate (but hopefully insightful) explanation for why you need to apply value after instantiating the widget, and why undefined needs to be wrapped. If you feel that I’ve omitted details or that some aspects could be made clearer, please let me know, and I’ll gladly update the post.

3 Likes

Oh my goodness, @mootari - this is amazing. Thank you for your time and for sharing these insights? :heart:

I’ll check back in soon to share what I’ve been working on. Still a long way to go… and never would I make it anywhere if it weren’t for this community. Thanks again!

P.S. This is amazing:

viewof r1 = Object.assign(radio({
  // options ...
}), {value: Promise.resolve(undefined)})

… With it, one can resolve any promise to a concrete placeholder value.

Hi again, @mootari - and thanks again!

Just a follow up:"

… This is interesting, but it requires that we create an entirely different button to uncheck a radio value. I keep playing around, but haven’t manage to re-apply this so that the input itself will allow me to ‘unclick’ (like click again and the input is unchecked).

When you say ‘only programatically’ - does this meas that we can only write another function (like the button) to change the behavior if clicked? [Hence necessitating the different button]?

The nature of radio buttons (no mechanism to deselect) makes them an awkward choice for optional selections. One pattern I’ve seen is to include an “n.a.” option that is selected by default. Note that you need to set an empty string as value. undefined won’t work here, because the string representation is used as value.

As far as I can tell, deselection cannot be implemented reliably for interactions with the radio itself or its label.

In any case, I believe that radio deselection via direct toggling serves no practical purpose: Deselecting radios isn’t a UI pattern that is used anywhere. If you intend for anyone besides yourself to use these form controls, then be assured that they’ll at best discover this mechanism only by accident.

If you need an optional selection from a list of choices, consider using a <select>, with an empty option selected by default.

The case I am thinking of is a person looking at some options, then decides “no, i don’t think this is how i feel” while having clicked one. For me indeed, yes, I would imagine that double clicking would de-select and re-set the field to blank. I agree that having an n/a field could serve the same purpose, but I feel that mentally there’s a distinction (indeed, for me) of ‘unchecking’ an option and just moving on. Radio buttons enforce a ‘select one and only one’ behavior - but if it’s instead inherent to other types of inputs, then maybe my question is instead whether I can adapt a multi-select to accept only 1, 2 or any other arbitrary number of options ?

The more important question, which everyone should ask themselves before beginning work on an implementation, is: “How is this supposed to work conceptually?”

At first, select elements may seem like a good candidate to base your widget on, but they aren’t. Let’s walk through a possible implementation, starting with a look at what we have to work with:

  • Out of the box you can allow one or multiple choices for selects.
  • When we allow multiple choices, there are three modes of selection:
    • click selects an option, but deselects every other selected option.
    • ctrl+click (cmd+click for macOS) selects or deselects additional options, without changing already selected options.
    • shift+click extends the closest range, but deselects all other ranges.
  • We can disable either individual options or the entire select.

As unintuitive as the selection modes for multiple choices sound, as much of a pain they are in practice. Here is a screenshot of two selects, with only one of them allowing multiple choices:


Right away we see no visual indication that multiple choices are possible. But let’s play Devil’s Advocate and try to work with that anyway:

  • When a user selects more than the allowed number of choices, we can respond with one of the following strategies:
    1. Ignore every selection past the max number.
    2. Completely ignore the input, even if some part of a range selection would still be valid.
    3. Unselect the oldest selection. Even more awkward, because we’d expect the user to both understand this behavior and mentally keep track of the order in which they made their choices.
    4. Unselect the farthest selection. Incredibly awkward, because the behavior is unintuitive, distance will often be a tie, and it’s debatable wether a range start or end should be used as measuring offset.
  • None of these behaviors are great, but we could probably get away with #2 if we combine it with some additional visual feedback.

“Of course!”, I hear you exclaim, “We simply mark all other options as disabled!”. Sure, we could, but then we’d prevent the user from making a different selection, even if that selection would cancel the previous selection:


Oh, and did I mention the awkwardness of allowing a single optional choice?


I’ll take a look at possible alternatives in the next comment.

1 Like

I didn’t dive into the existing research on usability for multiple limited selections. But what I can say is that the solution very much depends on the use case:

  • the overall number of choices available
  • the number of choices allowed
  • the type of options to choose from

There’s no one-size-fits-all. E.g., one might go for a pillbox selection (optionally with search) or drag’n’drop assignment.

In your case however we can probably get away with a simple list of checkboxes which we enhance twofold:

  • We add a notice about the maximum number of selectable options.
  • Once the limit has been reached, we disable all unchecked checkboxes.

Because checkboxes work through toggling and are changed one at a time, the user gets direct feedback to their actions:

  1. Checking the last available checkbox within the limit disables all remaining checkboxes.
  2. Unchecking any of the selected checkboxes reenables all disabled checkboxes.

I’ve created an example implementation here:

2 Likes

Hi @mootari and thank you for all the amazing insights! I apologize that I haven’t responded yet and will check back in shortly. I am rushing around now and wish to read your posts and look at your notebook more carefully. Certainly I’ve met few people who can provide so clear an explanation as you. Thank you!

2 Likes

Hi again, Fabian and others reading this,

You’re absolutely right, and from the beginning I should have included a more concrete example.

I have been trying to program a survey that a colleague made as an HTML document that I can show as a proof of concept. I am trying to be ‘true’ to the way the survey was initially designed (as I am also not an expert on surveys, though I know there’s a whole discipline to it).

Here’s where I arrived at:

And here’s how it looks in HTML:

https://aaronkyle.github.io/concept/ui-design/sandbox/survey-form-view.html

In this form, under the ‘no’ option, there are three choices that elaborate the meaning of ‘no’. In this case, the radio is great because it enforces that one choice is acceptable to describe the answer, and the user doesn’t have to click off a tick box to modify a choice. Under the ‘yes’ option, there are checkboxes, as several choices may be acceptable.

There’s so much more that I need to learn to really make this all work right, but for now I am just trying get something generally working that reflects what I am after. I asked the question about un-checking a radio because as in trying to sum together the values of these questions in a meaningful way, I kept reloading the radio input to clear the cells value. It was at this point that I inquired into un-checking, as I also imagined users feeling that none of the options adequately describe their view. In this case, I could do as you note and expose another field like an ‘other’ text area – and maybe that is the most simple and best solution. My ‘next step’ was going to be to trying to figure out how to adapt Mike’s ‘conditional forms’ notebook so that it could accept the ‘value’ ‘label’ combination (current it cannot), as that approach is better for disabling conditional inputs (my initial yes/no toggle).

… I also still have a lot of playing around to do in order to figure out how to meaningfully sum the values. I also think our general approach to that isn’t great, but for now just trying to return the values as supplied to me.

Your example for limited inputs is really cool, by the way.

So anyway - this is all just to share why I am asking and what I am trying to learn / achieve. Ideally the proof of concept will help to me to motivate our team to engage an inputs specialist to carry survey creation forward. :wink: