有没有想过如何建立一个像你在博客管理面板或概念文档中看到的那些花哨的标签输入组件?那么,不要再想了!在这篇文章中,我们将使用Vue 3的组合API来制作一个可重复使用的标签输入组件。在这篇文章中,我们将使用Vue 3的组合API来制作一个属于我们自己的可重复使用的标签输入组件。在这一过程中,我们将介绍一些你应该知道的重要概念,以便有效地使用Vue 3的组合API。
作为一个公平的警告,如果你只是在寻找一个开箱即用的解决方案,那就去抓一个现有的UI库或一个独立的标签输入,由一个有信誉的开发/设计团队定期维护。这篇文章中的标签输入组件不一定是为生产准备的。但如果你是来学习更多关于组合API和构建自定义可重用组件的内容,那么请继续阅读
我们正在构建的东西
下面是我们要建立的一个例子。标签输入允许你输入自定义标签,然后在按下回车键时将其提交给输入。你也可以通过在一个空的输入口(即一个当前没有输入标签的输入口)上后退,或者点击每个标签末尾的小x来删除标签。
该输入法还支持将标签限制为只有某些值,这些值将在焦点上显示,并在用户输入时被过滤掉。
最重要的是,这些功能将被建立起来,以便该组件可以重复使用,并有一个直观的界面,与普通的输入元素一样工作。
跟进和显示标签
现在,我们已经看到了我们正在构建的东西,让我们深入到代码中去吧在代码沙盒中,我使用Vue CLI来引导一个Vue 3项目。你也可以这样做,或者在你的本地机器上使用Vue CLI或Vite。然后,让我们把一些模板代码扔进一个单一的文件组件。
// components/TagInput.vue
<template>
</template>
<script>
export default {
setup(){
}
}
</script>
复制代码
接下来,让我们从Vue导入ref函数,用它来存储我们组件的现有标签。我们可以把它初始化为一个数组,里面有一些假的标签,这样我们就可以更好地看到我们正在使用的东西。最后,我们需要从setup函数中返回它,以便将其暴露在模板中。
<script>
import {ref} from 'vue'
export default {
setup(){
const tags = ref(['hello', 'world']);
return { tags }
}
}
</script>
复制代码
有了跟踪标签的方法,我们现在可以在模板中显示它们了。这可能是最令人费解的部分,因为没有本地输入法可以像我们希望的那样显示标签,但实际上这是相当简单的。我们可以使用一个无序的列表,在其中循环显示标签。然后,稍后我们可以使用一些自定义的样式和少量的javascript来让东西按照你所期望的那样在输入框上定位。(另外,我不是可访问性专家,所以如果你们是可访问性专家,如果这一点可以改进的话,请留下评论!?)
<template>
<div class="tag-input">
<ul class="tags">
<li v-for="tag in tags" :key="tag" class="tag">
{{ tag }}
</li>
</ul>
</div>
</template>
复制代码
这就产生了一个看起来很普通的无序列表。
然后,我们可以用一些自定义的样式把它打扮一下。
<style scoped>
ul {
list-style: none;
display: flex;
align-items: center;
gap: 7px;
margin: 0;
padding: 0;
}
.tag {
background: rgb(250, 104, 104);
padding: 5px;
border-radius: 4px;
color: white;
white-space: nowrap;
transition: 0.1s ease background;
}
</style>
复制代码
这些肯定开始看起来像标签了,但是输入在哪里呢?它正在到来,但我们需要先做一些基础工作。
新标签
现在,让我们创建一个叫做newTag
的变量,以跟上用户正在输入的当前标签。我是什么意思?这样想吧:一个标签输入已经有了 “hello “和 “world “的标签。这些标签被存储在tags
。然后用户开始输入 “foo”,但还没有按回车键提交标签。”foo “就是将被存储在我们新的newTag
变量中的内容。
<script>
import {ref} from 'vue'
export default {
setup(){
const tags = ref([]);
const newTag = ref('') //keep up with new tag
return { tags, newTag }
}
}
</script>
复制代码
现在我们可以在模板中添加一个输入,就在ul
的上方,并通过v-model将newTag
变量与之绑定。
<input v-model="newTag" type="text" />
<ul class="tags">...</ul>
复制代码
绑定到位后,当用户在输入框中输入时,newTag
的值将被更新,以反映输入框中的内容。
我们还可以添加一些样式,使输入的内容更加简洁。
input {
width: 100%;
padding: 10px;
}
复制代码
添加新标签
这让我们非常接近于添加一个新的标签由于我们在Javascript领域有现成的newTag
值,我们准备创建一个addTag
函数。这应该被定义在setup函数中。addTag
将接受一个要添加的标签作为参数(我们将在一分钟内把newTag
传给它)。然后我们将把该标签推入tags
变量。
setup(){
//...
const addTag = (tag) => {
tags.value.push(tag); // add the new tag to the tags array
};
}
复制代码
注意这里,在调用.push
时,你必须以tags.value
为目标。这是因为它是一个由Vue的ref
函数创建的反应性引用。当我们在模板中时,我们不需要考虑这个问题,因为.value
已经自动为我们处理。然而,在脚本标签中,如果我们想获得或设置tags
的值,我们需要以tags.value
。
最后,我们将重置newTag
的值,这样,一旦有新的标签加入,输入就会被清除,并为另一个标签做好准备。
const addTag = (tag) => {
tags.value.push(tag);
newTag.value = ""; // reset newTag
};
复制代码
有了addTag
函数,我们现在可以通过从setup函数中返回它来将其暴露在模板中。
setup(){
//...
return { tags, newTag, addTag }
}
复制代码
需要注意的是,从现在开始,为了简洁起见,我将不再提及需要从setup方法返回任何变量或函数。请将此作为你的警告,对于未来模板中需要的任何变量,请你自己去做。
现在我们可以将addTag
方法绑定到输入端的按键事件上,并将newTag
。我们还将使用enter
关键修改器,这样它就只在用户按下回车键时添加一个标签,而不是在每次按键时。
<input
...
@keydown.enter="addTag(newTag)"
/>
复制代码
为了改善用户体验,我们还将考虑到按下tab键来输入一个新标签。这次我们还需要添加prevent
修饰符,以便标签不会将焦点从输入中移开。
<input
...
@keydown.enter="addTag(newTag)"
@keydown.prevent.tab="addTag(newTag)"
/>
复制代码
删除一个标签
现在我们合乎逻辑的下一步是允许从输入中移除标签,所以让我们创建一个removeTag
函数。它的逻辑是相当简单的。它接收要移除的标签的索引,然后使用.splice
,从该索引的标签数组中移除一个项目。
const removeTag = (index) => {
tags.value.splice(index, 1);
};
复制代码
为了满足最佳的用户体验,我们将提供两种删除标签的方式。
- 允许在一个空的输入(即一个没有新标签值的输入)上后退,以删除列表中的最后一个标签
- 允许通过点击该标签上的
x
,删除特定的标签。
点击X来删除特定标签
让我们先处理第二种情况,因为它实际上是这两种情况中比较简单的。首先,我们可以在每个标签内添加一个x
按钮,将removeTag
函数绑定到它的点击事件。然后,可以通过修改v-for来访问传递该函数的索引。
<li v-for="(tag, index) in tags" :key="tag" class="tag">
{{ tag }}
<button class="delete" @click="removeTag(index)">x</button>
</li>
复制代码
最后,我们可以用一些样式使用户界面变得平滑。
.delete {
color: white;
background: none;
outline: none;
border: none;
cursor: pointer;
}
复制代码
移除最后一个标签的后移
为了移除最后一个标签,我们应该能够通过delete
修改器来调用输入的按键事件的removeTag
方法。对于要删除的索引,我们可以指定标签的长度减去1,因为数组的长度是0的索引。
<input
...
@keydown.delete="removeTag(tags.length - 1)"
>
复制代码
不幸的是,这还不够,因为现在只要用户按下退格键,最后一个标签就会被删除,即使他们只是想从新的标签中退格一个字符。
为了解决这个问题,我们可以检查新标签中是否有任何字符存在,如果没有,则只删除最后一个标签。
<input
...
@keydown.delete="newTag.length || removeTag(tags.length - 1)"
>
复制代码
定位标签
到目前为止,我们能够在输入框中输入新标签,并按回车键提交。然后,我们能够通过x按钮或后退的方式再次删除它们。祝贺你!在这一点上,我们已经完成了标签输入的基本功能。
还有一些可以添加的点缀,但仍然缺乏的一个明显的特征是标签的位置。现在,它们只是显示在输入的下面,这完全没有让人感觉到这是一个标签输入。他们需要在输入中出现。
大多数情况下,可以用一些CSS来处理。为了把标签放在输入的上方,我们可以把容器元素做成相对的,然后把标签ul绝对定位,把它从左边推到10px,以配合输入的padding。
.tag-input {
position: relative;
}
ul{
...
position: absolute;
top: 0;
bottom: 0;
left: 10px;
}
复制代码
我们还可以给ul一个75%的最大宽度,这样标签的右边总是有空间可以输入。然后任何溢出的标签都可以水平滚动。
ul {
...
max-width: 75%;
overflow-x: auto;
}
复制代码
定位光标
仅仅依靠CSS是不够的,因为我们仍然需要将光标推到输入的适当位置。否则,我们就会在任何现有的标签下面打字了
为了将光标推到合适的位置,我们需要在添加新标签时动态地更新输入端的左边填充。我们可以通过建立一个反应式引用来保持左填充的值。它将被初始化为10,因为那是整个输入的padding。
setup(){
//...
const paddingLeft = ref(10);
}
复制代码
然后我们需要将paddingLeft变量绑定到输入的样式属性上。
<input
...
:style="{ 'padding-left': `${paddingLeft}px` }"
/>
复制代码
现在有趣的部分来了!我们怎么知道什么时候该把左边的填充设置为什么?实际上,把它分解成这样一个问题,实际上是为了得到一个相当简单的答案。我们想把padding设置为tags ul的宽度(可能再加上一点,给它一些喘息的空间),而且我们想在tags ul的宽度发生变化时(即当一个tag被添加或删除时)这样做。
让我们先来处理获取宽度的问题。当你想在Vue中直接与DOM交互时,你需要创建一个模板引用,这在少数情况下是必要的。模板引用就像用ref
函数创建的反应式引用,但它不是引用原始的Javascript数据类型,而是引用一个DOM节点。要在Vue中创建一个模板引用,你所需要做的就是在你想访问的DOM元素上添加ref
属性,并给它一个名字来引用它。
<ul class="tags" ref="tagsUl">
复制代码
然后在setup函数中,你创建一个同名的reactive ref,并从setup方法中返回它,瞧,一旦组件被安装,你就可以访问脚本部分的DOM元素。
setup(){
//...
const tagsUl = ref(null)
return {
//...
tagsUl
}
}
复制代码
有了标签ul在手,我们现在可以创建一个函数来读取它的宽度并适当地设置paddingLeft变量。
const setLeftPadding = () => {
const extraCushion = 15
paddingLeft.value = tagsUl.value.clientWidth + extraCushion;
}
复制代码
现在,为了在正确的时间调用它,我们可以用深度选项观察tags
。深度选项是必要的,因为数组本身并没有被重新赋值,而是其中的成员发生了变化。我们还将使用nextTick来确保DOM的更新是完整的,标签ul的宽度是准确的
import { ref, watch, nextTick } from "vue";
export default{
setup(){
//...
watch(tags, ()=> nextTick(setLeftPadding), {deep: true});
}
}
复制代码
让我们也在组件安装时调用setLeftPadding函数。这将考虑到在做任何改变之前,输入上已经存在的任何像 “hello “和 “world “这样的标签。
import { ref, watch, nextTick, onMounted } from "vue";
export default{
setup(){
//...
onMounted(setLeftPadding)
}
}
复制代码
还有一件事我们可以在标签变化时处理,你可以从上面的gif图中看到。当标签溢出tags ul时,我们需要将tags ul滚动到最后,以便你能看到最近添加的标签。我们可以在设置左边的padding时做到这一点。
const setLeftPadding = () => {
//...
tagsUl.value.scrollTo(tagsUl.value.scrollWidth, 0);
};
复制代码
然后,为了使函数名称更合适,我们可以把它改为onTagsChange
。
const onTagsChange = () => {
// set left padding
const extraCushion = 15;
paddingLeft.value = tagsUl.value.clientWidth + extraCushion;
// scroll tags ul to end
tagsUl.value.scrollTo(tagsUl.value.scrollWidth, 0);
};
watch(tags, () => nextTick(onTagsChange), { deep: true });
onMounted(onTagsChange);
复制代码
使其可重复使用
到目前为止,还没有真正的方法来使用这个组件。它的值被设置在里面,然后就再也没有出来过。让我们来解决这个问题。
我们怎样才能让它感觉更像一个与v-model
一起工作的本地输入?Vue 3的文档指出,在 “3.xv-model
上的自定义组件相当于传递一个modelValue
道具并发出一个update:modelValue
事件”
因此,这意味着我们需要将标签的值初始化为一个modelValue
道具,当标签发生变化时发出一个update:modelValue
事件。
让我们来做这件事。首先,我们需要接受modelValue
道具,然后将tags
设置为它。
<script>
export default{
props:{
modelValue: { type: Array, default: ()=> [] },
},
setup(props){
const tags = ref(props.modelValue);
}
}
</script>
复制代码
这将使进入组件的值固定下来。为了再次将其发送出去,我们可以在标签改变时发出一个update:modelValue
事件。而恰好我们已经有了一个onTagsChange
的函数。
为了访问emit
,我们可以对上下文对象进行解构,它可以通过setup方法的第二个参数获得。
setup(props, {emit}){
//...
const onTagsChange = () => {
//...
emit("update:modelValue", tags.value)
}
}
复制代码
现在我们可以用v-model
与标签输入进行交互,感觉就像一个本地输入一样。
// App.vue
<template>
<div>
<TagInput v-model="tags" />
<ul>
<li v-for="tag in tags" :key="tag">{{ tag }}</li>
</ul>
</div>
</template>
<script>
import TagInput from "./components/TagInput.vue";
export default {
name: "App",
components: {
TagInput: TagInput,
},
data() {
return {
tags: ["Hello", "App"],
};
},
};
</script>
复制代码
总结
还有很多事情可以做,以加强我们的标签输入的功能,但由于我们到目前为止所维护的简单明了的代码库,添加这些新功能几乎是微不足道的。我在这个CodeSandbox中完成的一些功能包括。
- 标签选项
- 防止重复的标签
- 防止空标签
- 和显示标签的数量
如果你有兴趣,可以去看看代码,看看这些其他功能是如何实现的。如果你愿意接受挑战,可以分叉沙盒,创造你自己的超强功能(如果你这样做,别忘了在这里的评论中分享你的创造)!