乐趣区

关于vue.js:基于Vue指令系统实现输入框过滤文本数字

官网文档

一个自定义指令由一个蕴含相似组件生命周期钩子的对象来定义。钩子函数会接管到指令所绑定元素作为其参数。

指令的定义对象能够提供几种钩子函数:

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器利用前调用
  created(el, binding, vnode, prevVnode) {// 上面会介绍各个参数的细节},
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他本人的所有子节点都挂载实现后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他本人的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}}
钩子参数
  • el:指令绑定到的元素。这能够用于间接操作 DOM。
  • binding:一个对象,蕴含以下属性。

    • value:传递给指令的值。例如在 v-my-directive=”1 + 1″ 中,值是 2。
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 “foo”。
    • modifiers:一个蕴含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 {foo: true, bar: true}。
    • instance:应用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
思路

咱们能够通过 arg 传参管制过滤文本或者数字,通过 binding 中传参管制过滤逻辑。

过滤类型
export enum EDirectiveType {
  文本 = 'text',
  数字 = 'number',
}
文本过滤参数
/**
 * @param allowChinese 是否容许中文
 * @param allowSpace 是否容许空格
 * @param regExp 正则
 */
export interface IDirectiveTextBindingVO {
  allowChinese: boolean;
  allowSpace: boolean;
  regExp: RegExp;
}
数字过滤参数

/**
 * @param integral 整数位
 * @param decimal 小数位
 * @param negative 是否反对正数
 * @param min 最小
 * @param max 最大
 */
export interface IDirectiveNumberBindingVO {
  integral: number;
  decimal: number;
  negative: boolean;
  min: number | null;
  max: number | null;
}

实现

指令注册:
const registerInputFilter = (app: App) => {
  app.directive('inputFilter', {created(el, binding, vnode) {el._assign = getModelAssigner(vnode);
      const type = binding.arg;
      addEventListener(el, 'input', (e) => {if ((e.target as any).composing) return;
        let domValue: string = el.value;
        switch (type) {
          case EDirectiveType. 文本:
            domValue = handleTextFilter(domValue, binding);
            break;
          case EDirectiveType. 数字:
            domValue = handleNumberFilter(domValue, binding);
            break;
        }
        el._assign(domValue);
      });
    },
    // beforeMount(el, binding) {},
    // mounted 中解决是为了指令加载实现后登程过滤逻辑
    mounted(el, binding) {
      const type = binding.arg;
      let domValue: string = el.value;
      switch (type) {
        case EDirectiveType. 文本:
          domValue = handleTextFilter(domValue, binding);
          break;
        case EDirectiveType. 数字:
          domValue = handleNumberFilter(domValue, binding);
          break;
      }
      el.value = domValue;
    },
    // beforeUpdate(el, binding, vnode) {//   console.log('beforeUpdate---el, binding----------------------', el, binding);
    // },
    // updated(el, binding) {//   console.log('updated---el, binding----------------------', el, binding);
    // },
    // beforeUnmount(el, binding) {//   console.log('beforeUnmount---el, binding----------------------', el, binding);
    // },
    // unmounted(el, binding) {//   console.log('unmounted---el, binding----------------------', el, binding);
    // },
    deep: false,
  });
};

export default registerInputFilter;
文本过滤
/**
 * 过滤文本
 * @param domValue 文本内容
 * @param binding 指令传参
 * @returns
 */
function handleTextFilter(domValue: string, binding: DirectiveBinding<IDirectiveTextBindingVO>) {const { regExp, allowSpace} = binding.value;
  const characters: string = '';
  const defaultStr = String.raw`\`\\;\'\"<>`;
  const reg = new RegExp(String.raw`[${defaultStr}${characters}]`, 'g');
  domValue = domValue.replace(regExp instanceof RegExp ? regExp : reg, '');
  // 过滤空格
  if (!allowSpace) {domValue = domValue.replace(/\s+/g, '');
  }

  return domValue;
}
数字过滤
/**
 * 数字过滤
 * @param domValue 文本内容 
 * @param binding 指令传参
 * @returns 
 */
function handleNumberFilter(domValue: string, binding: DirectiveBinding<IDirectiveNumberBindingVO>) {const { decimal, negative, integral, min, max} = binding.value;
  const reg = new RegExp(String.raw`[^0-9${Math.ceil(decimal ?? 0) > 0 ? '.' : ''}${negative ?'-':''}]`, 'g');
  console.log('decimal------------------------------------------------------', decimal);
  console.log('negative-----------------------------------------------------', negative);
  console.log('integral-----------------------------------------------------', integral);
  domValue = domValue.replace(reg, '');
  let symbol = '';
  // 解决符号
  if (domValue.substring(0, 1) === '-') {
    symbol = '-';
    domValue = domValue.substring(1);
  }
  domValue = domValue.replace(/[^0-9\.]/g, '');

  // 解决首位小数点
  if (domValue.substring(0, 1) === '.') {domValue = `0${domValue}`;
  }

  // 禁止头部间断输出 0
  if (domValue.length > 1 && domValue.substring(0, 1) === '0' && domValue.substring(1, 2) !== '.') {domValue = domValue.substring(1);
  }

  // 解决小数点及小数位数
  if (domValue.includes('.')) {domValue = deduplicate(domValue, '.');
    const temp = domValue.split('.');
    domValue = `${temp[0]}.${temp[1]?.substring(0, (Math.ceil(decimal ?? 0) > 0 ? Math.ceil(decimal) : null) as number) ?? ''
    }`;
  }

  // 解决头部多余的 0
  if (domValue.length > 1) {domValue = domValue.replace(/^0+(?!\.)/, '');
  }

  // 限度整数长度
  const temp = domValue.split('.');
  temp[0] = temp[0].substring(0, Math.ceil(integral ?? 10));
  domValue = temp.length === 2 ? `${symbol}${temp[0]}.${temp[1]}` : `${symbol}${temp[0]}`;

  // 限度最大最小
  if (Object.prototype.toString.call(min) !== '[object Undefined]' &&
    Object.prototype.toString.call(min) !== '[object Null]' &&
    min &&
    domValue !== '' &&
    looseToNumber(domValue) < min
  ) {domValue = min + '';}
  if (Object.prototype.toString.call(max) !== '[object Undefined]' &&
    Object.prototype.toString.call(max) !== '[object Null]' &&
    max &&
    domValue !== '' &&
    looseToNumber(domValue) > max
  ) {domValue = max + '';}

  console.log('domValue-------------------------------', domValue);
  return domValue;
}
工具办法
/*
 * @Author: wanzp
 * @Date: 2023-04-26 21:15:54
 * @LastEditors: wanzp
 * @LastEditTime: 2023-05-05 20:22:19
 * @Description:
 */
import {VNode} from 'vue';

type AssignerFn = (value: any) => void;

export function onCompositionEnd(e: Event) {
  const target = e.target as any;
  if (target.composing) {
    target.composing = false;
    target.dispatchEvent(new Event('input'));
  }
}

export const invokeArrayFns = (fns: Function[], arg?: any) => {for (let i = 0; i < fns.length; i++) {fns[i](arg);
  }
};

export const getModelAssigner = (vnode: VNode): AssignerFn => {const fn = vnode.props!['onUpdate:modelValue'] || (__COMPAT__ && vnode.props!['onModelCompat:input']);
  return Array.isArray(fn) ? (value) => invokeArrayFns(fn, value) : fn;
};

/**
 * "123-foo" will be parsed to 123
 * This is used for the .number modifier in v-model
 */
export const looseToNumber = (val: any): any => {const n = parseFloat(val);
  return isNaN(n) ? val : n;
};

export function addEventListener(el: Element, event: string, handler: EventListener, options?: EventListenerOptions) {el.addEventListener(event, handler, options);
}

export function removeEventListener(
  el: Element,
  event: string,
  handler: EventListener,
  options?: EventListenerOptions,
) {el.removeEventListener(event, handler, options);
}

export const deduplicate = (target: string, symbol: string): string => {if (target.includes(symbol)) {const temp = target.split(symbol);
    let str = `${temp.shift() ?? ''}${symbol}`;
    temp.filter((v) => v).forEach((v) => (str += v));
    return str;
  }
  return target;
};
退出移动版