🏠 back to Observable

'All of the Above' using Observable inputs?`

I would like to implement a simple ‘all of the above’ checkbox using Observable inputs (and nothing else too fancy… seems most answers to this on the Internet involve jQuery).

I imagined this problem to be conceptually easy, but I didn’t understand the mechanics of the checkbox input. While I can set an array of values as the initial value for the notebook, I can’t figure out how to change the output to change all boxes with a corresponding value to ‘selected’.

Here’s my misguided attempt:

Each value is it’s own array object, so with my attempt the value comes out looking: Array(3) ["1", "2", "1,2,3,4,5,6"]

Makes sense. So my attempt fails. :frowning:

Is it possible to ‘overwrite’ the values or to ‘tick’ each box? Other easy avenues for ‘select all’ ?

It’s not exactly trivial, especially if you insist on adding the functionality on top of Jeremy’s widget. Regardless, here’s how you’d do it:

viewof options_list = {
  const options = {
    "1": "Red",
    "2": "Orange",
    "3": "Yellow",
    // ...
  };
  
  // Useful convention: Prefix variables with "$" if they contain DOM nodes.
  // Also useful to mark D3 and jQuery collections.
  const $form = checkbox({
    title: "Colors",
    description: "Please select your favorite colors",
    // Convert the options on the fly to {value, label} definitions.
    options: Array.from(Object.entries(options), ([value, label]) => ({value, label})),
    value: ["2"],
  });
  
  // We could also obtain a FormCollection via $form.elements, at the risk of
  // getting extra elements like submit buttons.
  const inputs = $form.querySelectorAll(`input[type="checkbox"]`);
  
  // Obtain the inline style from an input and its label.
  const inputCSS = inputs[0].style.cssText,
        labelCSS = inputs[0].parentElement.style.cssText;
  // Our "All of the above" checkbox. We'll wrap it in a label in another step.
  const $selectAll = html`<input type=checkbox style="${inputCSS}">`;
  // Inject our custom element and markup right between the last checkbox label
  // and the description.
  inputs[0].parentElement.parentElement.insertBefore(
    html`<div><label style="${labelCSS}">${$selectAll} All of the above`,
    inputs[inputs.length-1].parentElement.nextElementSibling
  );
  // Wrap the widget form so that we can expose our own .value property.
  const $wrap = html`<div>${$form}`;
  // What we'll return whenever $selectAll is checked.
  const allValues = Array.from(Object.keys(options));
  // Define a dynamic getter for .value to avoid having to maintain additional state.
  Object.defineProperty($wrap, 'value', {
    get() {
      return $selectAll.checked
        // If "All of the above" is checked, return all values.
        // We call .slice() to avoid handing out the same array reference over again.
        ? allValues.slice()
        // Else, Return the current widget value.
        : $form.value;
    }
  });
  // Usability improvement: Enable/disable all widget checkboxes whenever
  // "All of the above" gets toggled.
  // Important to note: We let the "input" event bubble up all the way to $wrap.
  // Also, function() is used instead of an arrow function so that we can comfortably
  // reference this instead of $selectAll.
  $selectAll.oninput = function(e) {
    for(const $i of inputs) $i.disabled = this.checked;
  };
  
  return $wrap;
}

Demo:
Demo

2 Likes

Wow, definitely not trivial! This is pretty amazing stuff though, thank you for your verbose explanations. I learned a ton, including:

  1. being refreshed on how to convert object definitions, and reading about Object.entries()
  2. learning about .querySelectorAll and how this will work for documents and elements
  3. How to grab out style information and re-attach it to an element
  4. How to construct an HTML element within JavaScript and how to insert it in a specific location
  5. Actually understanding the ? and : markings after return (this is really helpful)

… And on top of this, you also gave me a solution :slight_smile: Huge thanks!!

For completeness’ sake I’d like to point out that the html template tag function is provided by Observable’s Standard Library. If we had to write

$div = html`<div><label style="${labelCSS}">${$selectAll} All of the above`,

in pure Javascript, we’d need to either set the desired markup for an element and then inject our checkbox:

  const $div = document.createElement('div');
  $div.innerHTML = `<div><label style="${labelCSS}"> All of the above`;
  const $label = $div.querySelector('label');
  $label.insertBefore($selectAll, $label.firstChild);

or construct the DOM node by node:

  const $div = document.createElement('div');
  const $label = $div.appendChild(document.createElement('label'));
  $label.style.cssText = labelCSS;
  $label.appendChild($selectAll);
  $label.appendChild(document.createTextNode(' All of the above'));
1 Like