Dynamic datalist

Hi, I’m tinkering with the following text input:

This works with a static datalist. I’m wondering if there is a way to recompute such a datalist dynamically, according to the data that the user has just typed in the same textbox (e.g., a kind of typeahead triggering API calls).

1 Like

Here’s an example:

{
  const input = viewof state.querySelector('input[list]');
  const list = input.list;
  const oninput = e => {
    // do something
  };
  input.addEventListener('input', oninput);
  invalidation.then(() => input.removeEventListener('input', oninput));
}

I’m doubtful though that browsers can properly handle updates to a displayed datalist.

2 Likes

Thank you. IMO, a kind of “native” typeahead could be interesting for the Observable standard library.

1 Like

In the meanwhile, here is my prototype

In action:

PS: HTML <datalist> is based on ids. They must be unique. I designed the autocomplete form passing in a user-defined namespace to avoid id conflicts when you want more forms in the same notebook. Probably there is a better approach out there. Let me know!

1 Like

Nice! A few pointers:

  • You can get a unique ID via DOM.uid().id - this is the recommended approach within Observable
  • Don’t use <form> - there’s no point to it, and nesting forms creates an undefined state
  • Don’t use querySelector to retrieve your elements. Instead define them upfront and then interpolate:
    const input = htl.html`<input>`;
    const form = htl.html`<div>${input}`;
    
  • You may want to memoize options so that they don’t have to be recreated:
    const options = new Map();
    const getOption = name => options.get(name) || (options.set(name, htl.html`<option value=${name}>`), options.get(name));
    // ...
    list.appendChild(getOption(name));
    
  • Minor nitpick: I’d avoid <br> as it makes custom styling more difficult. Consider using inline styles or a dedicated style element with scoped rules. I’ve found that the following pattern works well to produce scoped styles:
    // Browsers don't support :scope, so :scope is equal to :root.
    // However we're going to replace it.
    const css = `:scope input {display:block}`;
    const scope = DOM.uid('my-form').id;
    const style = htl.html`<style>${css.replace(/:scope\b/g, `.${scope}`)}`;
    const form = html`<div class="${scope}">${style}`;
    
3 Likes

Hi @mootari. Sorry for my late reply. I refactored the code sticking to your suggestions. Thank you for sharing them!

I interpolated hypertext literals and I added memoization for the options. I used a slightly different approach. I use a Map to store each query (key) and its resultset (value), thus avoiding to fetch data from the source (e.g., from Web APIs) every time input changes. <option> elements are not cached but I managed to handle them with D3’s data.join. Final version in the same place.

1 Like