【2022-11-09】vue2实现简单的tag-input标签输入框

16 篇文章 0 订阅
16 篇文章 0 订阅

tag-input

如题,需要实现一个标签输入框,除去一些优秀的插件外,突然想到自己可以简单实现下,具体效果如下图展示(基本示例,限制为email类似输入,输入值校验提示信息未做):
tag-input

编码之前

基本思路

实现此类型的标签输入框,实际上并不是真正的在输入框中添加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
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值