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

介绍

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

在本文中,您将学习如何使用 制作自动完成组件v-model,如何使用键修饰符处理事件,以及如何为异步请求做好准备。

先决条件

要完成本教程,您需要:

本教程已通过 Node v15.3.0、npmv6.14.9 和vuev2.6.11 验证。

步骤 1 — 设置项目

出于本教程的目的,您将从使用@vue/cli.

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

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

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

  • cd vue-autocomplete-component-example

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

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

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
        (result)
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete'
};
</script>

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

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

src/components/SearchAutocomplete.vue
<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    min-height: 1em;
    max-height: 6em;    
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

步骤 2 — 过滤搜索结果

当用户键入时,您将希望向他们显示结果列表。您的代码需要侦听输入更改以了解何时显示这些结果。

为此,您将使用v-model. v-model是一个具有两种方式数据绑定的指令,用于表单输入和更新用户输入事件数据的文本区域。

因为您需要知道用户何时完成输入以过滤结果,所以您还将为@input.

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  data() {
    return {
      search: '',
    };
  },
  methods: {
    onChange() {
      // ...
    }
  }
};
</script>

现在您知道要搜索什么以及何时搜索,您将需要一些数据来显示。

就本教程而言,您将使用值数组,但您可以更新过滤器函数以处理更复杂的数据结构。

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

源代码/App.vue
<template>
  <div id="app">
    <SearchAutocomplete
      :items="[
        'Apple',
        'Banana',
        'Orange',
        'Mango',
        'Pear',
        'Peach',
        'Grape',
        'Tangerine',
        'Pineapple'
      ]"
    />
  </div>
</template>

<script>
import SearchAutocomplete from './components/SearchAutocomplete.vue'

export default {
  name: 'App',
  components: {
    SearchAutocomplete
  }
}
</script>

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

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    items: {
      type: Array,
      required: false,
      default: () => [],
    },
  },
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
    };
  },
  methods: {
    filterResults() {
      this.results = this.items.filter(item => item.toLowerCase().indexOf(this.search.toLowerCase()) > -1);
    },
    onChange() {
      this.filterResults();
      this.isOpen = true;
    }
  },
}
</script>

在 中filterResults(),注意 howtoLowerCase()应用于键入的文本和数组的每个元素。这确保用户可以使用大写或小写单词,并且仍然可以获得相关结果。

您还需要确保仅在用户输入内容后才显示结果列表。您可以通过使用v-show.

注意:使用v-show代替v-if的原因是这个列表的可见性经常会被切换,虽然 的初始渲染成本v-show更高,v-if但切换成本更高。

第 3 步 – 使用点击事件更新搜索结果

现在您需要确保结果列表可用。您将希望用户能够单击结果之一并自动将该值显示为所选值。

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

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

一旦发生这种情况,您还需要关闭结果列表以获得更好的用户体验:

src/components/SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  methods: {
    setResult(result) {
      this.search = result;
      this.isOpen = false;
    },
    // ...
  },
}
</script>

要在用户单击结果列表后关闭结果列表,您需要监听此组件外的单击事件。让我们在组件安装后完成,当用户单击某处时,您将需要检查它是否在组件之外。您将用于Node.contains检查事件目标是否属于该组件。

src/components/SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  destroyed() {
    document.removeEventListener('click', this.handleClickOutside);
  }
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.isOpen = false;
      }
    }
  },
}
</script>

此时,您可以编译您的应用程序:

  • npm run serve

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

第 4 步 – 支持箭头键导航

让我们为UP,DOWNENTER添加一个事件侦听器多亏了键修饰符,Vue 为最常用的键提供了别名,因此您无需验证每个键属于哪个键码。

为了跟踪选择了哪个结果,您将添加一个arrowCounter以保存搜索结果数组中当前索引的值。设置arrowCounterto的初始值-1以保证在用户主动选择之前没有选择任何选项。当用户按下该DOWN键时,该arrowCounter增加1。当用户按下该UP键时,该arrowCounter值减少 1。

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

您需要注意不要在列表结束后继续计数,也不要在结果可见之前开始计数。

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
      arrowCounter: -1
    };
  },
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.arrowCounter = -1;
        this.isOpen = false;
      }
    },
    onArrowDown() {
      if (this.arrowCounter < this.results.length) {
        this.arrowCounter = this.arrowCounter + 1;
      }
    },
    onArrowUp() {
      if (this.arrowCounter > 0) {
        this.arrowCounter = this.arrowCounter - 1;
      }
    },
    onEnter() {
      this.search = this.results[this.arrowCounter];
      this.arrowCounter = -1;
      this.isOpen = false;
    }
  },
}
</script>

我们还通过向所选选项添加活动 CSS 类来添加视觉辅助:

src/components/SearchAutocomplete.vue
<style>
  // ...

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

第 5 步 – 处理异步加载

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

注意:您可以在组件内部发出请求,但大多数应用程序已经使用特定的库来发出请求,无需在此处添加依赖项。

您需要对组件进行一些更改:

  1. 一个指示我们是否需要等待结果的指针
  2. 一旦输入的值发生变化,就向父组件发出一个事件
  3. 知道何时接收数据的观察者
  4. 通知用户的加载指示器
src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-if="isLoading"
        class="loading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    // ...
    isAsync: {
      type: Boolean
      required: false,
      default: false,
    },
  },
  // ...
  watch: {
    items: function (value, oldValue) {
      if (this.isAsync) {
        this.results = value;
        this.isOpen = true;
        this.isLoading = false;
      },
    }
  },
  // ...
  methods: {
    // ...
    onChange() {
      this.$emit('input', this.search);

      if (this.isAsync) {
        this.isLoading = true;
      } else {
        this.filterResults();
        this.isOpen = true;
      }
    },
    // ...
  },
}
</script>

此代码更改引入了一个isAsync道具。如果这存在于组件中,则将通过比较items来自服务器请求的请求提供的结果来过滤结果

第 6 步 – 结束项目

应用所有更改后,SearchAutocomplete.vue将类似于以下内容:

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
      @input="onChange"
      v-model="search"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
    />
    <ul
      id="autocomplete-results"
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        class="loading"
        v-if="isLoading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: 'SearchAutocomplete',
    props: {
      items: {
        type: Array,
        required: false,
        default: () => [],
      },
      isAsync: {
        type: Boolean,
        required: false,
        default: false,
      },
    },
    data() {
      return {
        isOpen: false,
        results: [],
        search: '',
        isLoading: false,
        arrowCounter: -1,
      };
    },
    watch: {
      items: function (value, oldValue) {
        if (value.length !== oldValue.length) {
          this.results = value;
          this.isLoading = false;
        }
      },
    },
    mounted() {
      document.addEventListener('click', this.handleClickOutside)
    },
    destroyed() {
      document.removeEventListener('click', this.handleClickOutside)
    },
    methods: {
      setResult(result) {
        this.search = result;
        this.isOpen = false;
      },
      filterResults() {
        this.results = this.items.filter((item) => {
          return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
        });
      },
      onChange() {
        this.$emit('input', this.search);

        if (this.isAsync) {
          this.isLoading = true;
        } else {
          this.filterResults();
          this.isOpen = true;
        }
      },
      handleClickOutside(event) {
        if (!this.$el.contains(event.target)) {
          this.isOpen = false;
          this.arrowCounter = -1;
        }
      },
      onArrowDown() {
        if (this.arrowCounter < this.results.length) {
          this.arrowCounter = this.arrowCounter + 1;
        }
      },
      onArrowUp() {
        if (this.arrowCounter > 0) {
          this.arrowCounter = this.arrowCounter - 1;
        }
      },
      onEnter() {
        this.search = this.results[this.arrowCounter];
        this.isOpen = false;
        this.arrowCounter = -1;
      },
    },
  };
</script>

<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

可通过CodePen获得现场演示

结论

在本文中,您使用 Vue.js 构建了一个自动完成组件。这是利用实现v-modelfilter@input@click@keydown,和.$emit

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

觉得文章有用?

点个广告表达一下你的爱意吧 !😁