# vue3自定义数字输入指令

415 阅读2分钟

日常的开发过程中可能需要限制用户输入的内容,比如只能输入数字和小数点,又或者是数字加-等等需求,不用废话直接上代码。

import { Directive, ref, DirectiveBinding, Ref, unref, App, nextTick } from 'vue';
import { ElMessage } from 'element-plus'
import { debounce } from 'lodash';
​
export type Obj = {
  maxLength?: number
  reg?: RegExp
}
let obj: Ref<Obj> = ref({})
const inputValue = ref()
let reg: RegExp | any = /[^\d]/g
​
​
// 警告提示信息
export function warnMsg(msgInfo: string, arg?: any) {
  ElMessage({
    type: 'warning',
    showClose: true,
    dangerouslyUseHTMLString: true,
    message: msgInfo,
    ...arg
  })
}
export function parseJson(jsonStr: string) {
  return JSON.parse(jsonStr, (k, v) => {
    try {
      // 将正则字符串转成正则对象
      if (eval(v) instanceof RegExp) {
        return eval(v);
      }
    } catch (e) {
      // nothing
    }
    return v;
  });
}
/**
 * json对象转json字符串
 * @param { Object } json json对象
 */
export function stringifyJson(json: { [key: string]: any }) {
  return JSON.stringify(json, (k, v) => {
    // 将正则对象转换为正则字符串
    if (v instanceof RegExp) {
      return v.toString();
    }
​
    return v;
  });
}
function tip() {
  if (reg.test(inputValue.value)) {
    warnMsg('请输入数字!')
  }
}
// 派发自定义事件
const trigger = (el: HTMLElement, type: any) => {
  const e = document.createEvent('HTMLEvents');
  e.initEvent(type, true, true);
  el.dispatchEvent(e);
}
​
export const onlyNumber: Directive = {
   mounted(el: any, binding: DirectiveBinding, vnode: any) {
    const input = el?.children[0];
    if (binding.arg) {
      try {
        obj.value = parseJson(binding.arg)
        if (unref(obj).maxLength) {
          input.maxLength = unref(obj)?.maxLength
        }
        if (unref(obj).reg) {
          reg = unref(obj)?.reg
        }
      } catch (error) {
      }
    }
    if (binding.value) {
      reg = binding.value;
    }
    input.oninput = function (e: any) {
      inputValue.value = this.value;
      this.value = this.value.replace(reg, '');
      if (unref(obj).maxLength) {
        if (this.value.length === unref(obj).maxLength) {
          warnMsg(`最大输入${unref(obj).maxLength}个字符`)
        }
      }
       // 问题代码 ---> input 改为 update:modelValue
      if (vnode.dirs.length) {
        vnode.dirs[0].instance.$emit('input', this.value)
      }
    };
    input.addEventListener('input', debounce(tip, 500))
  },
  beforeUnmount(el: any, binding: any, vnode: any) {
    const input = el;
    input.removeEventListener('input', tip)
  }
}
​
export default {
  install(app: App) {
    app.directive('onlyNumber', onlyNumber)
  }
}
​
​

正如上面代码所示原理很简单通过element-plus的输入框拿到dom,监听input事件,拿到输入的值之后,通过正则进行替换。所有的事情都已经准备完全了,我以为万事俱备了,正当我使用这个指令进行二次封装number输入框的时候,问题就出现了,在vue3中不能通过$emit来触发双向数据绑定。下面是我封装的number组件。

<template>
  <el-input v-bind="$attrs" v-onlyNumber:[arg] v-model="val">
    <template v-for="(index, name) in slots" :key="index + 'g'" #[name]>
      <slot :name="name"></slot>
    </template>
  </el-input>
</template><script setup lang="ts" name="OnlyNumberInput">
import { ref, computed, useSlots, PropType } from 'vue'
export type Arg = {
  maxLength?: number
  reg?: RegExp
}
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  },
  argOptions: {
    type: Object as PropType<Arg>,
    default: () => {
      // maxLength: 5,
      // reg: /[^\d,]/g
    }
  }
})
const emit = defineEmits(['update:modelValue'])
const arg = stringifyJson(props.argOptions)
const slots = useSlots()
const val = computed({
  get() {
    return props.modelValue
  },
  set(v) {
    emit('update:modelValue', v)
  }
})
​
/**
 * json对象转json字符串
 * @param { Object } json json对象
 */
function stringifyJson(json: { [key: string]: any }) {
  return JSON.stringify(json, (k, v) => {
    // 将正则对象转换为正则字符串
    if (v instanceof RegExp) {
      return v.toString();
    }
​
    return v;
  });
}
​
​
</script>

讲了这么多废话,到底会出现什么问题呢?

当我们正常输入值的时候如果不是正则匹配的数据就会被替换为空,当输入最后一个值的时候并不会替换为空虽然输入内没有显示,但值已经是为非正则匹配的值了,这不是我们想要的。

素材812.png

如何解决这个问题呢

  • 正常的解决方法就是实现element-plus中输入框的数据双向绑定,我想到通过自定义指令中的vnode拿到这个实例的emit进行触发input事件,在vue2中这是可行的,在vue3中并不能通过emit进行触发input事件,在vue2中这是可行的,在vue3中并不能通过`emit('input',this.value)来触发 可以通过 ***$emits("update:modelValue",this.value)`** *来触发

    • 如果不了解这个update:modelValue的朋友这里有图片
    • vue2vue3
      emits("update:name",value)emits("udpate:name",value)
      默认为 :value='' @input=""默认为 :modelValue="" @input=""
      组件只能允许一个v-model可以允许多个v-model绑定
    • 暂时就只能想到这么多了。

test.gif

总结

世上无难事,只要肯放弃就一定能解决问题。