如何使用 Vue.js 构建自动完成组件

介绍

自动完成是一个常见的现代功能. 当用户与输入字段进行交互时,他们将提供与他们输入的内容相关的建议值列表。

在本文中,您将学习如何使用v 模型创建自动完成组件,如何使用关键修改器处理事件,以及如何为非同步请求做好准备。

前提条件

要完成本教程,您将需要:

本教程已通过 Node v15.3.0、npm v6.14.9 和 vue v2.6.11 进行验证。

步骤1 - 设置项目

对于本教程的目的,您将从@vue/cli生成的默认Vue项目中构建。

1npx @vue/cli create vue-autocomplete-component-example --default

这将配置一个新的Vue项目,具有默认配置:Vue 2,babel,eslint

导航到新创建的项目目录:

1cd vue-autocomplete-component-example

现在,您可以使用代码编辑器创建一个新的自动完成组件,这将是一个单个文件的Vue组件,包含模板,脚本和风格。

为了构建自动完成组件,您的模板至少需要两件事:输入和列表。

 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      type="text"
 6    />
 7    <ul
 8      class="autocomplete-results"
 9    >
10      <li
11        class="autocomplete-result"
12      >
13        (result)
14      </li>
15    </ul>
16  </div>
17</template>
18
19<script>
20export default {
21  name: 'SearchAutocomplete'
22};
23</script>

此代码将创建一个div,输入用于输入文本和显示自动完成结果的未分类列表。

接下来,为相同文件中的组件提供一些样式:

 1[label src/components/SearchAutocomplete.vue]
 2<style>
 3  .autocomplete {
 4    position: relative;
 5  }
 6
 7  .autocomplete-results {
 8    padding: 0;
 9    margin: 0;
10    border: 1px solid #eeeeee;
11    height: 120px;
12    min-height: 1em;
13    max-height: 6em;    
14    overflow: auto;
15  }
16
17  .autocomplete-result {
18    list-style: none;
19    text-align: left;
20    padding: 4px 2px;
21    cursor: pointer;
22  }
23
24  .autocomplete-result:hover {
25    background-color: #4AAE9B;
26    color: white;
27  }
28</style>

第2步:过滤搜索结果

当用户类型时,您将需要向他们显示结果列表,您的代码将需要听取输入更改,以便知道何时显示这些结果。

要做到这一点,您将使用 v-modelv-model 是对表单输入和文本区域进行双向数据绑定的指令,该指令更新了用户输入事件的数据。

因为您需要知道用户何时完成打字来过滤结果,您还会添加@input的事件倾听器。

 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      v-model="search"
 6      @input="onChange"
 7      type="text"
 8    />
 9    <ul
10      class="autocomplete-results"
11    >
12      <li
13        class="autocomplete-result"
14      >
15      </li>
16    </ul>
17  </div>
18</template>
19
20<script>
21export default {
22  name: 'SearchAutocomplete',
23  data() {
24    return {
25      search: '',
26    };
27  },
28  methods: {
29    onChange() {
30      // ...
31    }
32  }
33};
34</script>

现在你知道要寻找什么以及何时去做,你需要一些数据来显示。

对于本教程的目的,您将使用一个数组值,但您可以更新过滤器函数来处理更复杂的数据结构。

打开App.vue,并修改它以导入和引用autocomplete组件:

 1[label src/App.vue]
 2<template>
 3  <div id="app">
 4    <SearchAutocomplete
 5      :items="[
 6        'Apple',
 7        'Banana',
 8        'Orange',
 9        'Mango',
10        'Pear',
11        'Peach',
12        'Grape',
13        'Tangerine',
14        'Pineapple'
15      ]"
16    />
17  </div>
18</template>
19
20<script>
21import SearchAutocomplete from './components/SearchAutocomplete.vue'
22
23export default {
24  name: 'App',
25  components: {
26    SearchAutocomplete
27  }
28}
29</script>

接下来,在代码编辑器中重新参阅SearchAutocomplete.vue,并添加代码来过滤搜索结果并显示它们:

 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      v-model="search"
 6      @input="onChange"
 7      type="text"
 8    />
 9    <ul
10      v-show="isOpen"
11      class="autocomplete-results"
12    >
13      <li
14        v-for="(result, i) in results"
15        :key="i"
16        class="autocomplete-result"
17      >
18        {{ result }}
19      </li>
20    </ul>
21  </div>
22</template>
23
24<script>
25export default {
26  name: 'SearchAutocomplete',
27  props: {
28    items: {
29      type: Array,
30      required: false,
31      default: () => [],
32    },
33  },
34  data() {
35    return {
36      search: '',
37      results: [],
38      isOpen: false,
39    };
40  },
41  methods: {
42    filterResults() {
43      this.results = this.items.filter(item => item.toLowerCase().indexOf(this.search.toLowerCase()) > -1);
44    },
45    onChange() {
46      this.filterResults();
47      this.isOpen = true;
48    }
49  },
50}
51</script>

filterResults()中,注意如何将toLowerCase()应用到输入的文本和数组的每个元素中,从而确保用户可以使用首字或下字单词,并且仍然可以获得相关的结果。

您还需要确保您仅在用户输入某个内容后才能显示结果列表,您可以通过使用 v-show来条件显示它。

<$>[注] **注:**使用v-show而不是v-if(https://vuejs.org/v2/guide/conditional.html#v-if-vs-v-show)的原因是,此列表的可见性通常会被转移,尽管v-show的初始渲染成本更高,但v-if的转移成本更高

步骤 3 – 通过点击事件更新搜索结果

现在,您需要确保结果列表可用,您将希望用户能够点击其中一个结果,并自动将该值显示为所选择的结果。

您可以通过 听到 click 事件并将值设置为搜索术语来实现:

 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      v-model="search"
 6      @input="onChange"
 7      type="text"
 8    />
 9    <ul
10      v-show="isOpen"
11      class="autocomplete-results"
12    >
13      <li
14        v-for="(result, i) in results"
15        :key="i"
16        @click="setResult(result)"
17        class="autocomplete-result"
18      >
19        {{ result }}
20      </li>
21    </ul>
22  </div>
23</template>

您还需要关闭结果列表一次,以获得更好的用户体验:

 1[label src/components/SearchAutocomplete.vue]
 2<script>
 3export default {
 4  name: 'SearchAutocomplete',
 5  // ...
 6  methods: {
 7    setResult(result) {
 8      this.search = result;
 9      this.isOpen = false;
10    },
11    // ...
12  },
13}
14</script>

要关闭结果列表,一旦用户单击了结果列表,您将需要听到该组件以外的点击事件. 让我们实现,一旦组件安装,当用户点击某个地方时,您将需要检查它是否在组件之外。

 1[label src/components/SearchAutocomplete.vue]
 2<script>
 3export default {
 4  name: 'SearchAutocomplete',
 5  // ...
 6  mounted() {
 7    document.addEventListener('click', this.handleClickOutside);
 8  },
 9  destroyed() {
10    document.removeEventListener('click', this.handleClickOutside);
11  }
12  methods: {
13    // ...
14    handleClickOutside(event) {
15      if (!this.$el.contains(event.target)) {
16        this.isOpen = false;
17      }
18    }
19  },
20}
21</script>

此时,您可以编写您的申请:

1npm run serve

然后,在网页浏览器中打开它,您将能够与自动完成组件进行交互,并确保它建议来自数组的项目。

步骤 4 – 支持箭头密钥导航

让我们为UPDOWNENTER密钥添加一个事件倾听器. 借助 key modifiers,Vue为最常用的密钥提供代码,因此您不需要验证哪个密钥代码属于每个密钥。

为了跟踪正在选择的结果,您将添加一个arrowCounter,以保留搜索结果阵列中当前索引的值。 将arrowCounter的初始值设置为-1,以确保用户在主动选择一个选项之前没有选择任何选项。

当用户按下ENTER键时,您将需要从结果组中获取该索引。

您需要小心不要在列表结束后继续计算,也不要在结果可见之前开始计算。

 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      v-model="search"
 6      @input="onChange"
 7      @keydown.down="onArrowDown"
 8      @keydown.up="onArrowUp"
 9      @keydown.enter="onEnter"
10      type="text"
11    />
12    <ul
13      v-show="isOpen"
14      class="autocomplete-results"
15    >
16      <li
17        v-for="(result, i) in results"
18        :key="i"
19        @click="setResult(result)"
20        class="autocomplete-result"
21        :class="{ 'is-active': i === arrowCounter }"
22      >
23        {{ result }}
24      </li>
25    </ul>
26  </div>
27</template>
28
29<script>
30export default {
31  name: 'SearchAutocomplete',
32  // ...
33  data() {
34    return {
35      search: '',
36      results: [],
37      isOpen: false,
38      arrowCounter: -1
39    };
40  },
41  methods: {
42    // ...
43    handleClickOutside(event) {
44      if (!this.$el.contains(event.target)) {
45        this.arrowCounter = -1;
46        this.isOpen = false;
47      }
48    },
49    onArrowDown() {
50      if (this.arrowCounter < this.results.length) {
51        this.arrowCounter = this.arrowCounter + 1;
52      }
53    },
54    onArrowUp() {
55      if (this.arrowCounter > 0) {
56        this.arrowCounter = this.arrowCounter - 1;
57      }
58    },
59    onEnter() {
60      this.search = this.results[this.arrowCounter];
61      this.arrowCounter = -1;
62      this.isOpen = false;
63    }
64  },
65}
66</script>

让我们通过将一个活跃的 CSS 类添加到所选选项中来添加视觉辅助:

 1[label src/components/SearchAutocomplete.vue]
 2<style>
 3  // ...
 4
 5  .autocomplete-result.is-active,
 6  .autocomplete-result:hover {
 7    background-color: #4AAE9B;
 8    color: white;
 9  }
10</style>

步骤 5 – 处理Async 加载

您还可以通过告知组件需要等待服务器的响应来加载结果来提供同步支持。

<$>[注] **注:**您可以在组件中提出请求,但大多数应用程序已经使用特定的 lib 来提出请求,这里不需要添加依赖性。

您将需要对组件进行几个更改:

  1. 一个指针来告知我们是否需要等待结果
  2. 发出一个事件给主组件一旦输入值发生变化
  3. 一个观察器来知道何时收到数据
  4. 一个加载指标来告知用户
 1[label src/components/SearchAutocomplete.vue]
 2<template>
 3  <div class="autocomplete">
 4    <input
 5      v-model="search"
 6      @input="onChange"
 7      @keydown.down="onArrowDown"
 8      @keydown.up="onArrowUp"
 9      @keydown.enter="onEnter"
10      type="text"
11    />
12    <ul
13      v-show="isOpen"
14      class="autocomplete-results"
15    >
16      <li
17        v-if="isLoading"
18        class="loading"
19      >
20        Loading results...
21      </li>
22      <li
23        v-else
24        v-for="(result, i) in results"
25        :key="i"
26        @click="setResult(result)"
27        class="autocomplete-result"
28        :class="{ 'is-active': i === arrowCounter }"
29      >
30        {{ result }}
31      </li>
32    </ul>
33  </div>
34</template>
35
36<script>
37export default {
38  name: 'SearchAutocomplete',
39  props: {
40    // ...
41    isAsync: {
42      type: Boolean
43      required: false,
44      default: false,
45    },
46  },
47  // ...
48  watch: {
49    items: function (value, oldValue) {
50      if (this.isAsync) {
51        this.results = value;
52        this.isOpen = true;
53        this.isLoading = false;
54      },
55    }
56  },
57  // ...
58  methods: {
59    // ...
60    onChange() {
61      this.$emit('input', this.search);
62
63      if (this.isAsync) {
64        this.isLoading = true;
65      } else {
66        this.filterResults();
67        this.isOpen = true;
68      }
69    },
70    // ...
71  },
72}
73</script>

如果它存在于组件中,将通过比较由服务器请求提供的项目来过滤结果。

步骤6 - 包装项目

随着所有更改的应用,‘SearchAutocomplete.vue’将看起来如下:

  1[label src/components/SearchAutocomplete.vue]
  2<template>
  3  <div class="autocomplete">
  4    <input
  5      type="text"
  6      @input="onChange"
  7      v-model="search"
  8      @keydown.down="onArrowDown"
  9      @keydown.up="onArrowUp"
 10      @keydown.enter="onEnter"
 11    />
 12    <ul
 13      id="autocomplete-results"
 14      v-show="isOpen"
 15      class="autocomplete-results"
 16    >
 17      <li
 18        class="loading"
 19        v-if="isLoading"
 20      >
 21        Loading results...
 22      </li>
 23      <li
 24        v-else
 25        v-for="(result, i) in results"
 26        :key="i"
 27        @click="setResult(result)"
 28        class="autocomplete-result"
 29        :class="{ 'is-active': i === arrowCounter }"
 30      >
 31        {{ result }}
 32      </li>
 33    </ul>
 34  </div>
 35</template>
 36
 37<script>
 38  export default {
 39    name: 'SearchAutocomplete',
 40    props: {
 41      items: {
 42        type: Array,
 43        required: false,
 44        default: () => [],
 45      },
 46      isAsync: {
 47        type: Boolean,
 48        required: false,
 49        default: false,
 50      },
 51    },
 52    data() {
 53      return {
 54        isOpen: false,
 55        results: [],
 56        search: '',
 57        isLoading: false,
 58        arrowCounter: -1,
 59      };
 60    },
 61    watch: {
 62      items: function (value, oldValue) {
 63        if (value.length !== oldValue.length) {
 64          this.results = value;
 65          this.isLoading = false;
 66        }
 67      },
 68    },
 69    mounted() {
 70      document.addEventListener('click', this.handleClickOutside)
 71    },
 72    destroyed() {
 73      document.removeEventListener('click', this.handleClickOutside)
 74    },
 75    methods: {
 76      setResult(result) {
 77        this.search = result;
 78        this.isOpen = false;
 79      },
 80      filterResults() {
 81        this.results = this.items.filter((item) => {
 82          return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
 83        });
 84      },
 85      onChange() {
 86        this.$emit('input', this.search);
 87
 88        if (this.isAsync) {
 89          this.isLoading = true;
 90        } else {
 91          this.filterResults();
 92          this.isOpen = true;
 93        }
 94      },
 95      handleClickOutside(event) {
 96        if (!this.$el.contains(event.target)) {
 97          this.isOpen = false;
 98          this.arrowCounter = -1;
 99        }
100      },
101      onArrowDown() {
102        if (this.arrowCounter < this.results.length) {
103          this.arrowCounter = this.arrowCounter + 1;
104        }
105      },
106      onArrowUp() {
107        if (this.arrowCounter > 0) {
108          this.arrowCounter = this.arrowCounter - 1;
109        }
110      },
111      onEnter() {
112        this.search = this.results[this.arrowCounter];
113        this.isOpen = false;
114        this.arrowCounter = -1;
115      },
116    },
117  };
118</script>
119
120<style>
121  .autocomplete {
122    position: relative;
123  }
124
125  .autocomplete-results {
126    padding: 0;
127    margin: 0;
128    border: 1px solid #eeeeee;
129    height: 120px;
130    overflow: auto;
131  }
132
133  .autocomplete-result {
134    list-style: none;
135    text-align: left;
136    padding: 4px 2px;
137    cursor: pointer;
138  }
139
140  .autocomplete-result.is-active,
141  .autocomplete-result:hover {
142    background-color: #4AAE9B;
143    color: white;
144  }
145</style>

可通过 CodePen进行实时演示。

结论

在本文中,您使用 Vue.js 构建了一个自动完成组件,通过使用v-model,filter,@input,@click,@keydown.$emit来实现这一目标。

如果您想了解有关 Vue.js 的更多信息,请参阅 我们的 Vue.js 主题页面以获取练习和编程项目。

Published At
Categories with 技术
Tagged with
comments powered by Disqus