making inputs reusable?

Harris L created a cool List Input, which I would like to adapt to accept Jeremey’s inputs. Specifically, I would like to define a generic set of values, and allow users to add list items by selecting those values.

If I create this inputs as viewof select_input = select(select_values) I am creating a single value and DOM element, so if I try using that in the List Input, it will fail – just dragging that input down the list:

I thought that I could get around this by defining a function that generates the input and then using that:

function selectFunction() {
  return select(select_values)
}

But this doesn’t work because the changes to the input values don’t persist (try the dropdown):

Here’s the notebook:

How does one create a “reusable” input element in this context?

… I realize that I keep asking variations on the same question, which is embarrassing for me and probably frustrating for you. I apologize. I feel that I am close to understanding the basics, but there’s just some bits still not clicking.

Thanks for any and all help!

1 Like

Sorry, I should have played more with the ‘See in Action’ example before posting. This takes me pretty far in understanding the pattern.

Here’s the functioning rendition:

Here’s the code:

viewof selectList5 = listInput({
  value: ['Spring', 'Winter'],
  defaultValue: null,
  input: (value, i, values) => html.fragment`
    <select style="margin-left: 1em;">
      <option value=${null}>---</option>
      ${select_values.map(d => html.fragment`   /// it's not exactly the same as a 'reusable input',
                                                                          /// but we can specify a 'reusable options list'
                                                                          /// and pass values from it input an HTML input
      <option value=${d} selected=${d === value}>
        ${d}
      </option>
    `)}
    </select>
  `,
})

I will leave this as ‘unsolved’ b/c I haven’t quite gotten this to jive with Jeremy’s inputs… and it would be really nice to be able just to plug them in vs. re-creating the input each time (especially more complex ones line autoSelect)

Here’s how to do it. First, create a new helper function:

function multiRow(inputs) {
  return function(value) {
    const $inputs = inputs(value);
    
    for(const [name, $input] of Object.entries($inputs)) {
      if(name in value) $input.value = value[name];
    }

    const $view = html`<div>${Object.values($inputs)}`;
    const props = Object.fromEntries(Object.entries($inputs).map(([name, $input]) => [
      name,
      {
        get: () => $input.value,
        enumerable: true,
      },
    ]));
    $view.value = Object.defineProperties({}, props);
    $view.onchange = e => {
      if(e.target === $view) return;
      e.stopPropagation();
      $view.dispatchEvent(new Event('change', {bubbles: true}));
    };

    return $view;
  }
}
  • The function receives a callback, which, when called, returns an object of form elements, where the property names correspond to the desired names in the values object.
  • The function returns a builder function which, when called, constructs a new form row.

Then use it like this:

viewof myList = listInput({input: multiRow(() => ({
  mySelect: DOM.select(['one', 'two']),
  myText: html`<input>`,
})), defaultValue: {mySelect: 'two'}})

Annotated version of the above:

function multiRow(inputs) {
  // Return the "input" callback for listInput().
  return function(value) {
    // Fetch the object of form elements.
    const $inputs = inputs(value);
    // Assign default values. Note: will only work for elements that have a value property.
    // Will not work for, e.g., checkboxes.
    for(const [name, $input] of Object.entries($inputs)) {
      if(name in value) $input.value = value[name];
    }
    // Drop all inputs into the wrapping container that handles the combined
    // value and manages events.
    const $view = html`<div>${Object.values($inputs)}`;
    // Transform the inputs object into an object of property definitions.
    const props = Object.fromEntries(Object.entries($inputs).map(([name, $input]) => [
      // The property name.
      name,
      // The property definition.
      {
        // Value getter. Return the input's value.
        get: () => $input.value,
        // Make the property visible.
        enumerable: true,
      },
    ]));
    // Construct the value object with its dynamic property getters.
    $view.value = Object.defineProperties({}, props);
    // Capture change events.
    $view.onchange = e => {
      // If we dispatched the event directly, do nothing.
      if(e.target === $view) return;
      // Catch change events that bubbled up from any of the inputs.
      e.stopPropagation();
      // Dispatch a new change event, this time from the wrapper so
      // that the wrapper value gets processed instead.
      $view.dispatchEvent(new Event('change', {bubbles: true}));
    };
    // Return wrapper.
    return $view;
  }
}
viewof myList = listInput({
  // Create the input callback.
  input: multiRow(function() {
    // The form row's elements, indexed by their name.
    return {
      mySelect: DOM.select(['one', 'two']),
      myText: html`<input>`,
    };
  }),
  // An example default value, matching the form element properties.
  defaultValue: {
    mySelect: 'two',
  }
})
3 Likes

Thank you! This is super helpful!