@ayfie/datalist

A javascript based datalist replacement library. It addresses most (if not all) the shortcomings of the HTML standard `<datalist>`element.

Usage no npm install needed!

<script type="module">
  import ayfieDatalist from 'https://cdn.skypack.dev/@ayfie/datalist';
</script>

README

Datalist module (vanilla)

Description

This is a javascript based datalist replacement library. It addresses most (if not all) the shortcomings of the HTML standard <datalist>element.

It is written in typescript, meaning that both javascript and typescript developers will get intellisense in modern IDEs.

Quick start

Installation

  • npm:

    npm install @ayfie/datalist

  • yarn:

    yarn add @ayfie/datalist

Sample

HTML:

<html>
    <head>
        ...
    </head>
    <body>
        <input id="my-input-field" />
    </body>
</html>

Code:

import { Datalist } from 'datalist'

const inputElement = document.getElementById('query')

const datalist = new Datalist(inputElement)

datalist.setOptions(['element 1', 'element 2'])


Table of Contents

1. Motivation

The Datalist module makes it easy to show an autocomplete suggestion-list along with your input element.

Very much like the HTML <datalist> element, in fact.

So, why not use a normal <datalist> then?

The answer is that the <datalist> tag has some important limitations, at least at the time of writing this, (May, 2021). Here are some of the features missing in the <datalist> element that motivated this library, along with a comparison:

Challenge 1: Combine datalist with `textarea` elements

Using a drop-down autocomplete with an input-field is probably the most common scenario. There are however cases where you want to add a drop-down for text-areas to. It was actually one of the major motivations for this library, as one of my use cases needed to combine the autocomplete with a textarea.

Not supported.

The datalist element was designed to support only the input object. The HTML W3C group probably felt that supporting textarea elements where either not a scenario at all or important enough. There are however some scenarios where supporting textarea elements would have been valid.

Supported.

Both textarea and input fields are both supported equally well.

Challenge 2: Customizing the styling on rendered options

It is a very common use-case to want the drop-down to be styled the same way as the rest of the application. It is also very frustrating when the styling changes between browsers and operating systems too.

Not supported.

The browser uses the operating system's own controls for rendering the datalist element on the html page. This means that neither the datalist or the option elements can be styled. Which severely limits its usage in any page that wants to have its UI be the same cross platform - or even have a consistent UI for the web-application within the same browser and OS.

Supported.

Fully stylable presentation. It has default styling added that is made to resemble the look and feel of the datalist/option element as rendered in Chrome running on a Windows computer. You can also add your own inline styling in the settings object. If you don't like to use inline styles you can turn them off. You can always also add CSS for the class-names that is added to the DOM elements that the datalist instance generates.

Challenge 3: Specify position of the option-list
Not supported.

The datalist element is always fixed underneath the input-element (AFAIK). The width seems to always adjust according to its content, not the parent.

Supported.

The datalist options render underneath the input-element, the same way as the datalist element. There are extra settings for indicating where and how the options are to be rendered.

The default is to render the options under the input-element that it is tied to, which is the "DOWN" option. But, you can choose to render them "UP", "LEFT" and "RIGHT" of the input-element instead if you want to.

Challenge 4: Specify min length before showing options

You may not always want to show all options immediately when you start typing, or even just when clicking the input-field. Controlling the number of characters that needs to be entered before showing the options can be very important in regards to the user experience.

Not supported.

The datalist element shows options as soon as you start typing (or when you click in the input-field with the mouse button). There is no way to ask it to delay suggestions to after the user has typed a minimum of characters.

Supported.

The datalist instance can control this via the minLength setting. Since that setting defaults to 0, it shows the same default behavior as the W3C datalist element.

Challenge 5: Custom matching on options based on input-element value

There are many scenarios where you may want to control which options to show in regards to whether they are actually matching or not. Even a simple thing as to separate between case sensitivity could make a big difference. Another common use case is wanting to show only options that start with the same text.

Not supported.

The datalist element only filters options by checking if they contain the text written in the input field. Which means that if the input field contains "st", the following option would match: <option>test</option>. This is not always what you would want.

Supported.

The datalist has a setting that controls the filter for how matching is performed. The default is set to match the W3C datalist behavior, but you can easily replace the matching algorithm with your own, allowing perfect control of what is matching and not.

Challenge 6: Custom rendering of options

There are lots of scenarios where you want the presented options-list to show more than just the string.

Not supported.

The datalist element is rendered according to its predefined specifications. That is - always as a single string in a list. If you wanted to add other content to the option-entries, you would not be able to do that.

Supported.

The Datalist module has a setting where you can supply your own method for rendering the content of each option renderOption().

Note: The Datalist instance will always render the base structure of the list as a <ol><li></li>...</ol> list. But, the content within the <li> element is up to the renderOption() method. The default is to just render the string itself, thus mimicking the behavior of the W3C datalist.

Challenge 7: Exclude options that are not exact matches

One typical use case is to show i.e. countries in a dropdown and reduce the options as the user types in what they are looking for. For this scenario you would want to show 'Norway', also when the user has written 'Norway', to indicate that this is a valid choice. When you use the datalist as an autocomplete for i.e. a search.field then you don't want to suggest words they have already written. So for that scenario, 'Norway' should not be shown as an option when the input field contains 'Norway'.

Not supported.

The datalist element always includes options that matches the text in the input element.

Supported.

The Datalist module by default includes options that matches the text in the input element (to mimic the W3C datalist by default). But, by setting includeFullMatch: false in the settings, the options dropdown will exclude exact matches.

Challenge 8: Tie the options-list to the JSON results of a web url
Partly.

The datalist element has no options for binding the options to a url. But, with some extra code, re-generating the options list dynamically is possible.

Partly.

The Datalist module has no setting for doing this directly neither. But, since it is API-based this should be a more trivial task than with the W3C datalist. Just fetch the remote url results and pass these to the options when creating the Datalist. If needed the options can also be changed at run-time, as one could re-fetch the external url and call setOptions() at any time.

2. W3C <datalist> compliance

Navigation of the datalist elements is according to the W3C specifications, with an addition of being able to set the min-length before actuation as well as to customize the filter-method:

  • The datalist elements to show are given using these rules:
    • The elements given programmatically, filtered as follows:
      • When inputElement has content longer than the given minLength, the list is filtered based on the given algorithm (default indexOf(text) > -1)
  • When the inputElement is not focused:
    • When navigating to the inputElement via keys, thus giving focus to the element, the datalist remains HIDDEN.
    • When the inputElement is clicked on thus giving it focus however, the datalist is SHOWN (if has matching items).
  • When the inputElement is focused:
    • When the datalist is in a hidden state:
      • When the inputElement looses focus, the datalist remains HIDDEN.
      • When using UP or DOWN arrows the datalist IS shown, making the first/last element "selected".
    • When the datalist is in a visible state:
      • When the inputElement looses focus, the datalist is HIDDEN.
      • When using UP or DOWN arrows the datalist remains shown, making the higher or lower element "selected". When at the top or bottom the selected item will cycle.
      • When clicking ESCAPE the datalist becomes HIDDEN.
      • When clicking ENTER the selected datalist entry is written to the inputElement and the datalist is HIDDEN.
      • When hovering the mouse over the list, the current changes according to the mouse hover.
      • When selecting an item by clicking on it with the mouse the datalist entry is written to the inputElement and the datalist is HIDDEN.

3. Usage

const datalist = new Datalist(inputElement, {
    settings,
    callbacks,
    options: [
        'Alabama',
        'Alaska',
        'Arizona',
        'Arkansas',
        // ...
    ],
})

If you want the Datalist to return a different value than what is displayed that is also possible:

const datalist = new Datalist(inputElement, {
    settings,
    callbacks,
    options: [
        { display: 'Alabama', value: 'AL' },
        { display: 'Alaska', value: 'AK' },
        { display: 'Arizona', value: 'AZ' },
        { display: 'Arkansas', value: 'AR' },
        // ...
    ],
})

This simulates the way the <datalist> tag allows for the same feature: (<option value=”AL”>Alabama</option>...)

4. Features

4.1 Settings

There are many ways that you can adjust how the Datalist instance is created, by adjusting the settings property object in the constructor.

  • [[autoWidth | settings.autoWidth]]
  • [[includeFullMatch | settings.includeFullMatch]]
  • [[minLength | settings.minLength]]
  • [[position | settings.position]]

Also, if you want to change the styling of the Datalist instance via inline-styles, then this can easily be done via the [[IStyling | settings.styling]] property.

Please see the [[ISettings]] section to learn more about the settings you can pass when creating the Datalist instance.

4.2 Callbacks

  • [[onFilteredOptionsChanged]]
  • [[onOptionHighlighted]]
  • [[onOptionListChanged]]
  • [[onSelected]]
  • [[onVisibilityChanged]]

Please see the [[ICallbacks]] section to learn more about callbacks you can register when creating the Datalist instance.

4.3 Instance API

  • You can change the options to render at runtime using the [[Datalist.setOptions]] method.
  • You can force the options to hide with the [[Datalist.hide]] method.
  • You can force the options to show with the [[Datalist.show]] method (given that there are any matching options to render).

5. Caveats & Gotchas

5.1. Input type

The Datalist module does not care about the input type (<input type="...">). It will literally try to match the input field value with whatever options that have been added. Which only makes sense for textual properties. It will probably get very confused when used with for example an <input type="range"> element. The range element will render something similar to an progress-bar, and the options passed will match the value, when it is set. Thus rendering the option list (with one option) as each of the range values matches. There is nothing in the code that checks the type, so this is something that might give unpredictable results.

5.2 DOM manipulation

This library does manipulate the DOM in order to create the visual elements that are to mimic the <datalist> and the <option> elements. Also, in order to manipulate the positioning of the options a wrapper is also added as a parent of the input element.

More specifically, the DOM is augmented as follows (for position="DOWN" - which is default):

Before:

<input type="input">

Note: The above <input> can also be a <textarea>.

After:

<div class="DL-wrapper>
  <input type="input">
  <div class="DL-datalist-container">
    <ol class="DL-datalist">
      <li class="option>...</li>
      ...
    </ol>
  </div>
</div>

Note: The above example shows the modification for position == "DOWN". For other positions the datalist element is placed before the <input>-field.

5.3 Using with frameworks/libraries

This library does not render the items using a html-like template or JSX and it does not handle the events like for instance React expects it to. It directly manipulates the DOM by adding some extra elements. But, even though the DOM is manipulated, it does not automatically mean that it cannot be used with any of the frameworks and libraries. Most of these libraries/frameworks allows you to get a reference to the actual DOM instance of the object, which you can then use to call this library. Note though that if the i.e. React element is re-rendered, and the input field-is recreated the object reference will have changed. Thus the Datalist instance would also need to be recreated. Since it is recreated the state would not survive and the user would potentially be interrupted while navigating the options. It is normally considered bad practice to change the input field in such a way that it has to re-render. That would also introduce other problems, like if you were navigating the input field with left and right arrow-keys. That state would be lost and reset.

Because of this you should be safe. Just remember to get a reference to the field after the normal rendering is done. Then construct the Datalist instance using that reference. The reference element is moved inside a wrapper, but it is still valid after rendering.

The primary use of this library is actually as an add on to a Preact rendered textarea-field, where we use hooks to get the reference to its DOM instance. We have never had a problem with this library doing javascript based DOM autocomplete options.

5.4 Styling

Because the DOM is manipulated, any existing CSS rules that target your input-field or elements around it may be adversely affected. Fixing these issues are often fairly trivial though as you just have to change the rules to take into account that the input-field is now in this wrapper, and that surrounding elements are no longer siblings of the the input-field.

Notes:
Since the datalist-options are rendered with style position:absolute it doesn't out-of-the-box restrict to the width of the input-element. To make sure that the width is correct we monitor the input-element and make sure that it has the same width. This behavior can be turned off (see [[autoWidth]]).

5.5. Input key events

The Datalist module depends on monitoring all keys pressed while the input-element is focused. In particular the module is handling escape, enter and key up and down events. When appropriate it will steal the event from the control itself. Like, when using Enter to select an option, then the Enter key should not be handled by the input-element. This is done by stopping the event in the bubbling phase and thus making the event never happen in the input-element. If you however catch events in the bubbling phase yourself on the input-element, the Datalist event-handler might not be first to be executed so. This means that you may or may not get the Enter event, even if the Datalist cancels it. Also, if you get the event first - and you cancel it, the Datalist will not be able to convert the Enter event to an "option-select" command.

6. License

The MIT License

Copyright © 2021 Ayfie Group

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.