【2022-11-09】vue2实现简单的tag-input标签输入框
vue2实现tag-input
·
tag-input
如题,需要实现一个标签输入框,除去一些优秀的插件外,突然想到自己可以简单实现下,具体效果如下图展示(基本示例,限制为email类似输入,输入值校验提示信息未做):
编码之前
基本思路
实现此类型的标签输入框,实际上并不是真正的在输入框中添加tag标签,而是前端视觉上进行的伪造,实际上是是输入框输入值后,进过输入合法性校验后,回车活输入框失焦后,将输入值以tag标签的形式展示在输入框之前:
- 页面布局需要实现类似于在input中增加tag的视觉效果
- input输入框使用正常的v-model绑定值
- 实际展示为tag的值需要经过校验后,添加到实际的页面元素中
- 删除已输入的tag,需要将实际展示的tag集合中对应的数据删除,页面元素中也要移除
- 获取到的整体tag值为点击获取时存在的每一个合法输入的值的集合
- input输入框校验可以使用一些组件库中提供的方式(例如element-ui中利用form表单的方式进行输入校验)本例中使用校验方式具体参见代码
实现代码
代码简陋,实现仅作为demo,具体功能视业务需要做具体调整。
<template>
<div
@click="focusNewTag()"
:class="{
'read-only': readOnly,
'vue-input-tag-wrapper--active': isInputActive,
}"
class="vue-input-tag-wrapper"
>
<span v-for="(tag, index) in innerTags" :key="index" class="input-tag">
<span class="views_tag">{{ tag }}</span>
<a v-if="!readOnly" @click.prevent.stop="remove(index)" class="remove">
<slot name="remove-icon" />
</a>
</span>
<input
v-if="!readOnly && !isLimit"
ref="inputtag"
:placeholder="placeholder"
type="text"
v-model="newTag"
v-on:keydown.delete.stop="removeLastTag"
v-on:keydown="addNew"
v-on:blur="handleInputBlur"
v-on:focus="handleInputFocus"
class="new-tag"
/>
</div>
</template>
<script>
/* eslint-disable */
const validators = {
email: new RegExp(/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
url: new RegExp(/^(https?|ftp|rmtp|mms):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i),
text: new RegExp(/^[a-zA-Z]+$/),
digits: new RegExp(/^[\d() \.\:\-\+#]+$/),
isdate: new RegExp(/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/)
};
/* eslint-enable */
export default {
name: "InputTag",
props: {
value: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: ""
},
readOnly: {
type: Boolean,
default: false
},
validate: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: String | Function | Object,
default: "email"
},
addTagOnKeys: {
type: Array,
default: function () {
return [
13, // Return
188, // Comma ','
9 // Tab
];
}
},
addTagOnBlur: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: -1
},
allowDuplicates: {
type: Boolean,
default: false
},
beforeAdding: {
type: Function
}
},
data () {
return {
newTag: "",
innerTags: [...this.value],
isInputActive: false
};
},
computed: {
isLimit: function () {
return this.limit > 0 && Number(this.limit) === this.innerTags.length;
}
},
watch: {
value () {
this.innerTags = [...this.value];
}
},
methods: {
focusNewTag () {
if (this.readOnly || !this.$el.querySelector(".new-tag")) {
return;
}
this.$el.querySelector(".new-tag").focus();
},
handleInputFocus () {
this.isInputActive = true;
},
handleInputBlur (e) {
this.isInputActive = false;
this.addNew(e);
},
async addNew (e) {
const keyShouldAddTag = e ? this.addTagOnKeys.indexOf(e.keyCode) !== -1 : true;
const typeIsNotBlur = e && e.type !== "blur";
if ((!keyShouldAddTag && (typeIsNotBlur || !this.addTagOnBlur)) || this.isLimit) {
return;
}
let tag = this.beforeAdding ? await this.beforeAdding(this.newTag) : this.newTag;
console.log('tag', tag)
const isValid = await this.validateIfNeeded(tag);
if (tag && isValid && (this.allowDuplicates || this.innerTags.indexOf(tag) === -1)) {
tag += ';'
this.innerTags.push(tag);
this.newTag = "";
this.tagChange();
e && e.preventDefault();
}
console.log('this.innerTags~~~', this.innerTags)
},
validateIfNeeded (tagValue) {
if (this.validate === "" || this.validate === undefined) {
return true;
}
if (typeof this.validate === "function") {
return this.validate(tagValue);
}
if (typeof this.validate === "string" && Object.keys(validators).indexOf(this.validate) > -1) {
return validators[this.validate].test(tagValue);
}
if (typeof this.validate === "object" && this.validate.test !== undefined) {
return this.validate.test(tagValue);
}
return true;
},
remove (index) {
this.innerTags.splice(index, 1);
this.tagChange();
},
removeLastTag () {
if (this.newTag) {
return;
}
this.innerTags.pop();
this.tagChange();
},
tagChange () {
this.$emit("update:tags", this.innerTags);
this.$emit("input", this.innerTags);
}
}
};
</script>
<style>
.vue-input-tag-wrapper {
background-color: #fff;
border: 1px solid #ccc;
overflow: hidden;
padding-left: 4px;
padding-top: 4px;
cursor: text;
text-align: left;
-webkit-appearance: textfield;
display: flex;
flex-wrap: wrap;
}
.vue-input-tag-wrapper .input-tag {
background-color: #89ced5;
border-radius: 2px;
border: 1px solid #89ced5;
color: #fff;
display: inline-block;
font-size: 13px;
font-weight: 400;
margin-bottom: 4px;
margin-right: 4px;
padding: 3px;
}
.vue-input-tag-wrapper .input-tag:hover .remove {
/* display: inline-block; */
opacity: 1;
}
.vue-input-tag-wrapper .input-tag .remove {
/* display: none; */
cursor: pointer;
font-weight: bold;
color: #a51a1a;
opacity: 0;
}
.vue-input-tag-wrapper .input-tag .remove:hover {
text-decoration: none;
}
.vue-input-tag-wrapper .input-tag .remove:empty::before {
content: " x";
}
.vue-input-tag-wrapper .new-tag {
background: transparent;
border: 0;
color: #777;
font-size: 13px;
font-weight: 400;
margin-bottom: 6px;
margin-top: 1px;
outline: none;
padding: 4px;
padding-left: 0;
flex-grow: 1;
}
.vue-input-tag-wrapper.read-only {
cursor: default;
}
</style>
尾巴
点滴记录,汇聚江河
更多推荐
已为社区贡献4条内容
所有评论(0)