没有外部库的JavaScript自动完成

50

有没有不依赖其它库的JavaScript自动完成库?

我正在制作一个需要保持轻量级的移动应用程序,因此不使用jQuery或类似工具。


2
http://www.webreference.com/programming/javascript/gr/column5/index.html - Peter Ajtai
@PeterAjtai 这个链接已经失效了。 - Lesego M
6个回答

35

这里有一个基本的 JavaScript 示例,可以修改为自动完成控件:

var people = ['Steven', 'Sean', 'Stefan', 'Sam', 'Nathan'];

function matchPeople(input) {
  var reg = new RegExp(input.split('').join('\\w*').replace(/\W/, ""), 'i');
  return people.filter(function(person) {
    if (person.match(reg)) {
      return person;
    }
  });
}

function changeInput(val) {
  var autoCompleteResult = matchPeople(val);
  document.getElementById("result").innerHTML = autoCompleteResult;
}
<input type="text" onkeyup="changeInput(this.value)">
<div id="result"></div>


为了日后读者的利益,我已经实现了一个专门用于此目的的库(http://github.com/uohzxela/fuzzy-autocomplete)。但是它确实依赖于jQuery。不过,一个纯JS版本正在开发中。 - uohzxela
嗨!非常喜欢你的回答!有两个问题。1- 缓存编译表达式是否重要或必要?我问这个问题是因为看到了这篇文章:http://www.dustindiaz.com/autocomplete-fuzzy-matching 2- 我该如何限制它从数组中返回x个元素? - Jessica
2
.join('\\w*\\s*\\w*')替换.join('\\w*')将使得可以匹配来自不同单词的字母。 - ellockie
1
这很遗憾,这只是自动建议而不是自动完成。您无法选择建议。 - Andre Elrico
这里有一个与此代码示例松散相关的 codepen。它使用jQuery来过滤大约500个元素。表现良好。当使用vanilla js时应该会表现得更好。 - Nils Lindemann
显示剩余3条评论

31

对于任何在2017年以后需要简单解决方案的人,你可以使用HTML5内置的<datalist>标签,而不是依赖JavaScript。

例如:

<datalist id="languages">
  <option value="HTML">
  <option value="CSS">
  <option value="JavaScript">
  <option value="Java">
  <option value="Ruby">
  <option value="PHP">
  <option value="Go">
  <option value="Erlang">
  <option value="Python">
  <option value="C">
  <option value="C#">
  <option value="C++">
</datalist>

<input type="text" list="languages">

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/datalist


我认为这会比JS更快,因为它是一个浏览器API。不过我还没有进行任何基准测试,如果你有非常大的数据集,我建议你进行基准测试。我只用过它来处理小列表(200-300项),不需要任何花哨的东西(请注意,对于更复杂的设置,您将需要使用JavaScript或两者的组合)。 - Optimae
1
截至2021年4月20日,上述示例在Safari中也可以工作。 - Kaelan Dawnstar
谢谢,我已经相应地更新了,删除了关于Safari的那一行。 - Optimae

9

我计划使自动完成从本地字典、远程AJAX调用或两者混合的方式工作(因此已使用的单词将从远程调用添加到本地字典中),或者使用移动设备上现有的字典。我认为数据源不是自动完成脚本的责任,而是一个单独的问题。对我来说,制作自己的最复杂的部分是以有效的方式搜索可能匹配的数组。我想象一种B树算法,但宁愿使用经过深思熟虑的代码来完成这个任务,而不是自己编写。 - Billy Moon
好的,我明白了。如果您使用远程调用,则查询将交给数据源处理,而不是在JavaScript代码中处理。如果字典是本地的,那么我不知道(我猜这将取决于您的规则:匹配开头,匹配字符串的任何部分?)。我一直在使用John Resig博客上的几篇文章作为参考来做这样的事情,例如http://ejohn.org/blog/revised-javascript-dictionary-search/。 - Christophe
我已经编辑了我的答案并添加了参考资料。我正在使用第二个来进行本地过滤。 - Christophe
1
我之前读了那些链接。第一个解决方案依赖于jQuery,所以我跳过了它。第二个非常有趣(以及前面的两篇文章),但不幸的是它并不完全符合我需要的,因为我必须匹配单词的开头,而不是整个单词。我可能会将其用作自己解决方案的起点,因为他已经做了所有基准测试工作来找出什么是有效的,我可能只需进行最少量的编辑即可。感谢您的研究。 - Billy Moon
我最终使用第二个链接作为自己解决方案的起点,这个方案非常高效 - 但可能目前功能上还有所欠缺。非常感谢! - Billy Moon

3
ES2016新特性:无需外部库即可使用Array.prototype.includes方法。
function autoComplete(Arr, Input) {
    return Arr.filter(e =>e.toLowerCase().includes(Input.toLowerCase()));
}

Codepen演示


你好 @ronaldtgi, 根据"https://www.w3schools.com/jsref/jsref_includes.asp"中的信息,IE 11及更早版本不支持includes函数。 那么在includes无法使用时可以使用什么替代呢? - Ashish Shah
1
嗨@AshishShah,你也可以使用**indexOf()**。例如function autoComplete(Arr, Input) { return Arr.filter(e=>e.toLowerCase().indexOf(Input.toLowerCase()) !== -1); } - ronaldtgi

1

我之前研究过这个问题,我的解决方案最初类似于这里的ES6解决方案,就像这样:

return this.data.filter((option) => {
    return option.first_name
        .toString()
        .toLowerCase()
        .indexOf(this.searchTerms.toLowerCase()) >= 0
})

但这样做的问题在于它不够强大,无法处理过滤嵌套数据的情况。您可以看到它正在过滤类似以下数据结构的this.data
[
    { first_name: 'Bob', },
    { first_name: 'Sally', },
]

您可以看到,它基于this.searchTerms进行过滤,同时将搜索词和option.first_name转换为小写字母,但是它太过死板,无法搜索option.user.first_name。我的最初尝试是传递要过滤的字段,例如:

this.field = 'user.first_name';

但是这需要使用原始的自定义JavaScript来处理类似于this.field.split('。')的内容并动态生成过滤函数。

相反,我想起了一个我以前用过的叫做fuse.js的旧库,它很好地工作,因为它不仅处理了我刚才称之为this.field的任意嵌套情况,而且还根据定义的阈值处理模糊匹配。

在这里查看:https://fusejs.io/

[编辑说明]:我意识到这个问题正在寻找没有外部库,但我想保留这篇文章,因为它提供了相邻的价值。 这不是“解决方案”。

以下是我当前的使用方式:

import Fuse from 'fuse.js';

const options = {
    threshold: 0.3,
    minMatchCharLength: 2,
    keys: [this.field],
};

const fuse = new Fuse(this.data, options);

this.filteredData = fuse.search(this.searchTerms);

你需要阅读Fuse文档以更好地理解,但基本上,你可以看到使用数据和选项创建了一个new Fuse()对象来进行过滤。

keys: [this.field]部分很重要,因为这是你传递要搜索的键的位置,你可以传递一个数组。例如,你可以通过keys: ['user.first_name', 'user.friends.first_name']来过滤this.data

我目前在Vue JS中使用它,所以我将上面的逻辑放在实例watch函数中,因此每次this.searchTerms更改时,该逻辑都会运行并更新this.filteredData,然后将其放入我的自动完成组件的下拉列表中。

还有,很抱歉我刚才才意识到这个问题特别说明了不使用外部库,但我仍然会发布这篇文章,因为每次我在Vue JS或React JS中制作ES6自动完成时都会遇到这个问题。我认为严格或宽松模糊匹配非常有价值,并支持任意嵌套数据。根据Webpack包分析器,fuse.js经过gzip压缩后只有4.1kb,因此它非常小,而且可以支持“所有”客户端过滤需求。

如果您在使用外部库的能力上受到限制,请考虑我的第一个代码示例。如果您的数据结构是静态的,并且可以轻松更改option.first_name以类似option[this.field]的方式来变量化搜索字段(即:如果您的对象始终是平面的),则它可以正常工作。
您也可以将要搜索的列表变量化。尝试像这样做:
const radicalFilter = ({ collection, field, searchTerms }) => {
    return collection.filter((option) => {
        return option[field]
            .toString()
            .toLowerCase()
            .indexOf(searchTerms.toLowerCase()) >= 0
    })
}

radicalFilter({
    collection: [{ first_name: 'Bob' }, { first_name: 'Sally' }],
    field: 'first_name',
    searchTerms: 'bob',
})

根据我过去几年的经验,上面的示例非常高效。我在一个react-table组件中用它来过滤10000条记录,它毫不费力。它不会创建任何额外的中间数据结构。它只是使用Array.prototype.filter()对您的数组进行筛选,并返回匹配项的新数组。


0

我曾经通过向服务器发送JSON请求,并使用Python代码进行自动完成来实现这一点。虽然速度有点慢,但它避免了传输大量的数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接