构建最小的 AutoSuggest

2025-05-25

构建最小的 AutoSuggest

Web 花了好几年才引入<datalist>-tag,而它对于创建最广泛使用的 UI 组件之一“AutoSuggest”至关重要。在本教程中,我们将构建一个极简的“AutoSuggest”,使用JavaScript和不使用JavaScript 两种方式。


在我读过的最早的 UI 设计书籍之一,1995 年出版的《Windows 软件界面设计指南》中,它被称为组合框——因为它是下拉列表文本输入框组合而成。我个人认为这个术语比“自动建议”或“预先输入”更有意义,但似乎大家已经选择了“自动建议”——所以就沿用这个名称吧!

jQueryUI 有一个“AutoComplete”插件,名字不太对,因为“自动完成”是一个稍微不同的东西,如下图所示的UX Stackexchange帖子所示:

nXHX2


基本结构

在网上看到的大多数例子中, a<datalist>和 一起使用<input type="text">。但我更喜欢用<input type="search">。为什么?因为这种类型开箱即用,增加了一些很棒的、额外的、方便访问的功能:

  • 按下该Escape键可清除列表选择,再次按下该键可完全清除输入。

  • 在 Chrome 和 Safari 中,当您按下或,或者单击小“重置十字”时,会触发一个事件 - onsearch 。EscapeEnter


标记

这些建议本身<option>如下<datalist>

<datalist id="browsers">
  <option value="Edge">
  <option value="Firefox">
  <option value="Chrome">
  <option value="Opera">
  <option value="Safari">
</datalist>
Enter fullscreen mode Exit fullscreen mode

在 Chrome 中,也支持这种格式:

<option value="MSE">Microsoft Edge</option>
Enter fullscreen mode Exit fullscreen mode

value都会innerText显示在列表中,但只有value当您选择一个项目时才会插入。

<datalist>要将与输入链接起来,只需获取id并用作list-attribute:

<label>
  <strong>Pick a browser</strong>
  <input
    autocomplete="off"
    autocorrect="off"
    list="browsers"
    spellcheck="false"
    type="search">
</label>
Enter fullscreen mode Exit fullscreen mode

我们不想autocompletespellcheck干扰,所以我们将它们设置为offfalseautocorrect是仅限 Safari 的属性,在这种情况下也应该禁用。


CSS

这里没什么内容。我们可以用它来-webkit-appearance: none清除默认的浏览器样式,并添加我们自己的样式。以下是一个例子:

[type="search"] {
  border: 1px solid #AAA;
  font-size: 1rem;
  margin-block: 0.5rem;
  min-inline-size: 20rem;
  padding: 0.5rem 0.75rem;
  -webkit-appearance: none
}
Enter fullscreen mode Exit fullscreen mode

您可能确实想要更改的是那个重置输入的小“十字图标”:

重置十字

我在 中使用了一个 SVG 图标url(),并将其存储在CSS 自定义属性中,因此它可以同时用作mask-image-webkit-mask-image以实现浏览器兼容性:

[type="search"]::-webkit-search-cancel-button {
  --reset: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.016 15.609l-3.609-3.609 3.609-3.609-1.406-1.406-3.609 3.609-3.609-3.609-1.406 1.406 3.609 3.609-3.609 3.609 1.406 1.406 3.609-3.609 3.609 3.609zM12 2.016q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055 7.055-2.93z"/></svg>');
  background-color: currentColor;
  display: block;
  height: 1rem;
  mask-image: var(--reset);
  width: 1rem;
  -webkit-appearance: none;
  -webkit-mask-image: var(--reset);
}
Enter fullscreen mode Exit fullscreen mode

<input>Chrome 添加了带有的下拉箭头<datalist>,我们可以将其隐藏:

}
[list]::-webkit-calendar-picker-indicator {
  display: none !important;
}
Enter fullscreen mode Exit fullscreen mode

那里好多了:

重置交叉样式

在移动设备上,<input type="search">会触发带有“搜索”按钮的虚拟键盘。如果您不想这样,请查看inputmode

在 iPhone 上,<datalist>显示如下:

iphone数据列表

远非完美,但仍然比许多自定义解决方案要好得多,其中虚拟键盘使“AutoSuggest”上下跳动!


这就是简约的、无 JavaScript 的AutoSuggest!

对于国家选择器等功能来说非常棒— — 并且比 jQueryUI 的“自动完成”插件占用的最小224kb(包括它的 CSS 和 jQuery 本身)要好得多。

但是如果您想使用 API 并<option>动态创建 s 该怎么办?


添加 API

在查看 JavaScript 之前,让我们先向标记添加一些额外的属性<input type="search">

data-api="//domain.com?q="
data-api-cache="0"
data-api-key="key"
min-length="3"
Enter fullscreen mode Exit fullscreen mode

data-apiurl我们想要的fetch()

搜索文本将附加到此处。

可以data-api-cache0(禁用)或1(启用)。如果启用,<datalist>则初始 之后的 -options 将不会被覆盖fetch(),并且当您输入更多文本时,<datalist>将使用 的原生浏览器过滤。

data-api-key是您想要搜索并显示为 s 的结果对象中的“键/属性” <option>

min-lengthfetch()是一个标准属性。在本例中,它表示在触发之前需要输入多少个字符。


JavaScript

对于 JavaScript,我将解释我正在使用的所有方法,以便您可以使用所需的功能构建自己的定制AutoSuggest 。

首先,我们添加一个函数,autoSuggest(input)它只有一个参数:input

接下来,一个布尔值指示是否应该使用缓存:

const cache = input.dataset.apiCache - 0 || 0;
Enter fullscreen mode Exit fullscreen mode

返回的数据将存储在:

let data = [];
Enter fullscreen mode Exit fullscreen mode

为了不导致服务崩溃,我们需要一个debounce方法来过滤掉事件:

export default function debounced(delay, fn) {
  let timerId;
  return function(...args) {
    if (timerId) clearTimeout(timerId);
    timerId = setTimeout(() => { fn(...args); timerId = null }, delay)
  }
}
Enter fullscreen mode Exit fullscreen mode

我们存储对以下内容的引用<datalist>

const list = document.getElementById(input.getAttribute('list'));
Enter fullscreen mode Exit fullscreen mode

eventListener…并在上添加input

input.addEventListener('input', debounced(200, event => onentry(event)));
Enter fullscreen mode Exit fullscreen mode

200是方法中使用的延迟debounce。您可以修改它,或者将其添加到设置对象或类似的对象中。

最后,有一个onentry从内部调用的方法debounce

const onentry = async function(event) {
  const value = input.value.length >= input.minLength && input.value.toLowerCase();
  if (!value) return;
  if (!data.length || cache === false) {
    data = await (await fetch(input.dataset.api + encodeURIComponent(value))).json();
    list.innerHTML = data.map(obj => `<option value="${obj[input.dataset.apiKey]}">`).join('')
  }
}
Enter fullscreen mode Exit fullscreen mode

它是一个异步函数,首先检查输入是否包含最少数量的字符。如果没有,则直接返回。

如果没有数据存在,或者缓存设置为,则会触发0: falsea ,并更新s。fetch()<option>

太棒了,我们现在有了动态选项和一个最小化的脚本,只有497 字节,压缩后大约为349 字节!

但我觉得它缺少一些功能。我想在从列表中选择一个选项时触发一个自定义事件,并希望在该事件中显示匹配搜索结果中的对象。

让我们onentry稍微修改一下方法。我们可以使用 来event.inputType检测用户何时点击列表项,或者使用 来选择它Enter

if (event.inputType == "insertReplacementText" || event.inputType == null) {
  const option = selected(); 
  if (option) input.dispatchEvent(new CustomEvent('autoSuggestSelect', { detail: JSON.parse(option.dataset.obj) }));
  return;
}
Enter fullscreen mode Exit fullscreen mode

-methodselected查找并返回对象数组中的当前输入文本:

const selected = () => {
  const option = [...list.options].filter(entry => entry.value === input.value);
  return option.length === 1 ? option[0] : 0;
}
Enter fullscreen mode Exit fullscreen mode

现在 — — 在另一个脚本中!— — 我们可以监听该事件:

input.addEventListener('autoSuggestSelect', event => { console.info(event.detail) });
Enter fullscreen mode Exit fullscreen mode

如果我们想重置列表怎么办?在 Safari 和 Chrome 中,有一个onsearch-event 事件,它会在 reset 和 时触发Enter
让我们添加一个reset()-method :

const reset = () => { data = []; list.innerHTML = `<option value="">` }
Enter fullscreen mode Exit fullscreen mode

当用户点击“重置十字”或按下时触发它Escape

input.addEventListener('search', () => input.value.length === 0 ? reset() : '// Do something on Enter');
Enter fullscreen mode Exit fullscreen mode

<option>-method中的空格reset()是针对 Firefox 和 Safari 的 hack,否则在使用 dynamic 时会出现一些问题<datalist>。因此,在标记中默认添加一个空选项是个好主意:

<datalist id="suggest"><option value=""></option></datalist>
Enter fullscreen mode Exit fullscreen mode

该脚本压缩后大小为544 字节。我们还能做些什么吗?

在 Firefox 中,我们可以添加一个小的“polyfill” onsearch

if (!('onsearch' in input)) {
  input.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') { input.value = ''; reset(); }
    if (event.key === 'Enter') { ... }
  })
}
Enter fullscreen mode Exit fullscreen mode

还有什么?

您可以继续自行添加内容。但在此之前,让我们先添加一个settings-object 来保存我们已有的配置参数——以及您想要添加的任何内容!首先,我们将修改 main 函数:

autoSuggest(input, args)
Enter fullscreen mode Exit fullscreen mode

然后,我们将其合并args到设置对象中:

const settings = Object.assign({
  api: '',
  apiCache: false,
  apiKey: ''
}, datasetToType(args));
Enter fullscreen mode Exit fullscreen mode

datasetToType是一个小的辅助函数,它将数据集条目转换为正确的类型(以 为前缀的非字符串值:):

export default function datasetToType(obj) {
  const object = Object.assign({}, obj);
  Object.keys(object).forEach(key => {
    if (typeof object[key] === 'string' && object[key].charAt(0) === ':') {
      object[key] = JSON.parse(object[key].slice(1));
    }
  });
  return object;
}
Enter fullscreen mode Exit fullscreen mode

这样,我们可以autoSuggest使用标准 JavaScript 对象调用方法:

autoSuggest(input, { apiCache: false });
Enter fullscreen mode Exit fullscreen mode

— 或者它的dataset

autoSuggest(input, input.dataset);
Enter fullscreen mode Exit fullscreen mode

在标记中,我们将 替换0:false1将 替换为:true

data-api-cache=":false"
Enter fullscreen mode Exit fullscreen mode

我们还需要用 替换input.dataset.apisettings.api删除cache常量,然后用 替换它settings.cache(以及其他各个地方,请查看最后的示例!),但我们现在有一个settings-object,我们可以扩展新功能。


限制选择

你想限制value允许列表中的值吗?让我们扩展一下settings-object:

invalid: 'Not a valid selection',
limit: false
Enter fullscreen mode Exit fullscreen mode

我们将添加一个新方法:

const limit = () => {
  const option = selected();
  input.setCustomValidity(option ? '' : settings.invalid);
  if (!input.checkValidity()) {
    input.reportValidity();
    console.log('invalid');
  }
  else {
    console.log('valid');
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,我们将更新onsearch事件:

input.addEventListener('search', () => input.value.length === 0 ? reset() : settings.limit ? limit() : '');
Enter fullscreen mode Exit fullscreen mode

此方法使用 HTML5 的默认验证 API — 目前不执行任何操作(除了记录到console!)。您可以/应该对其进行调整,以使用您自己的方式处理无效状态。


示例

第一个例子是 DAWA,一个丹麦的地址查找服务(尝试输入“park”):

<label>
  <strong>DAWA - Danish Address Lookup</strong>
  <input
    autocomplete="off"
    autocorrect="off"
    data-api="//dawa.aws.dk/adresser/autocomplete?side=1&per_side=10&q="
    data-api-cache=":false"
    data-api-key="tekst"
    data-limit=":true"
    list="dawa"
    minlength="3"
    spellcheck="false"
    type="search">
</label>
<datalist id="dawa"><option value=""></option></datalist>
Enter fullscreen mode Exit fullscreen mode

下面是 JSON 占位符(尝试输入“lorem”):

<label>
  <strong>JSON placeholder</strong>
  <input
    autocomplete="off"
    autocorrect="off"
    data-api="//jsonplaceholder.typicode.com/albums/?_limit=10&q="
    data-api-key="title"
    list="jsonplaceholder"
    minlength="3"
    spellcheck="false"
    type="search">
</label>
<datalist id="jsonplaceholder"><option value=""></option></datalist>
Enter fullscreen mode Exit fullscreen mode

autoSuggest对具有关联的所有元素运行 -method 的快速方法<datalist>是:

import autoSuggest from './autosuggest.mjs';
const inputs = document.querySelectorAll('[list]');
inputs.forEach(input => {
  if (input.dataset.api) {
    input.addEventListener('autoSuggestSelect', event => { console.info(event.detail) });
    autoSuggest(input, input.dataset);
  }
})
Enter fullscreen mode Exit fullscreen mode

结论

这并非一个久经考验的“AutoSuggest”,您可以直接在项目中使用。它更像是一套原则和理念,您可以根据自己的需求进行定制,打造自己的专属功能:无论是极简还是功能丰富!

更重要的是,它旨在展示如何使用内置标签及其内置功能的“本机优先”方法通常可以减少 JavaScript 的使用和开销。

我创建了一个代码库,你可以从中获取演示文件。在VS Code中打开文件夹,然后使用Live Server或类似工具启动它。现场演示在这里

文章来源:https://dev.to/madsstoumann/building-a-minimal-autosuggest-4big
PREV
颜色是数学:它们如何匹配——以及如何构建颜色选择器
NEXT
表格样式指南