使用react/vue封装只能输入数字的输入框

前言

写业务的同学多多少少会碰到一些输入框只能输入数字的需求,再者就是数字格式化千分位等等,基于这些需求,我将使用 vue2vue3 以及react实现这一功能。

需求分析

  1. 输入框只能输入数字
  2. 允许小数存在(可选)
  3. 允许负数存在(可选)
  4. 格式化千分位(可选)

封装组件的优点

  1. 显示的时候带千分位,实际上获取到的值是不带千分位
  2. 抽离相同逻辑,减少代码冗余
  3. 使用更方便,代码维护性强

效果图如下:

vue2 二次封装 el-input

为了方便,我们直接使用element-ui组件库中的el-input来做二次封装,封装涉及的知识点:

  • $attrs
    • 透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。
    • 默认情况下,若是单一根节点组件,$attrs 中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置 inheritAttrs 选项来显式地关闭该行为。
    • 参考 透传 Attribute
  • 过滤器filters
  • 计算属性computed
  • 父子传参 this.$emit
  • v-model 语法糖
    • v-model 实际上是个语法糖,什么叫语法糖 ?顾名思义,吃起来很甜,用起来很方便
    • v-bind:value 和 v-on:input 的组合就是v-model
    • <input v-model="name" /> 等价于 <input :value="name" @input="e => name = e.target.value" />
js 复制代码
<template>
  <el-input 
    :value="_value" 
    @input="handleInput" 
    @blur="handleBlur" 
    v-bind="$attrs"
  />
</template>
 
<script>
export default {
  name: 'el-input-number',
  props: {
    value: {
      type: String,
      default: ''
    },
    // 是否支持负数,即支持输入 -,默认可以为负数
    isMinus: {
      type: Boolean,
      default: true,
    },
    // 是否是整数,即不能输入小数.默认可以输入小数
    isInteger: {
      type: Boolean,
      default: false,
    },
    // 显示的时候是否格式化为带千分位的值,默认格式化
    isThousands: {
      type: Boolean,
      default: true,
    }
  },
  filters: {
     // 格式化千分位
    _number(num) {
      let str = num.toString()
      let flag = false
      if (/^-?\d+(.\d+)?$/.test(str)) {
        if (str.includes('.')) {
          str = str.replace(/\.(\d+)$/, ',$1')
        } else {
          str += ',00'
          flag = true
        }
        while (/\d{4}/.test(str)) {
          str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
        }
        if (flag) {
          str = str.replace(/\,(\d+)$/, '')
        } else {
          str = str.replace(/\,(\d+)$/, '.$1')
        }
      }
      return str
    }
  },
  methods: {
    // 输入非数字结尾的做特殊处理
    handleBlur() {
      if (['.', '-'].includes(this.value)) {
        this.$emit('input', '');
      } else {
        const [integer, decimal] = this.value.split('.')
        let _value = this.value
        if (!decimal /* || decimal.length === 2 && decimal === '00' */) { // 小数不存在或空或.00 ,转为整数
          _value = integer
        } else if (decimal.length === 1) { // 有一位小数,后补0
          _value = `${this.value}0`
        }
        if (_value === '-0') {
          _value = '0'
        }
        this.$emit('input', _value)
      }
    },
    // 属性控制正则限制输入
    getRegexp(a, b) {
      let key = `${!!a}-${!!b}`
      const map = { // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
        'true-true': /[^0-9\-]/g, // 正负整数
        'true-false': /[^0-9]/g  , // 正整数
        'false-true': /[^0-9\.-]/g, // 正负数(含整数和小数)
        'false-false': /[^0-9\.]/g // 正数(含整数和小数)
      }
      return map[key]
    },

    handleInput(value) {
      const regexp = this.getRegexp(this.isInteger, this.isMinus)
      const _value = value
      .replace(regexp, '') // 根据属性限制输入
      .replace(/^(-?[^-]*)-*/g, '$1') // 只能开头一个 -
      .replace(/\.{2,}/g, '.') // 只能有一个.
      .replace(/(\.\d{1,2})(.*)$/g, '$1') // 1.12.12312 => 1.12 || .12.32 => .12
      .replace(/^(-?)\./, '$10.') // 以 . 或 -. 开头的 => 0. || -. => -0.
      .replace(/^(-?)0([0-9]+)/, '$1$2') // 正负整数部分不能以0开头

      this.$emit('input',_value)

    },
  },
  computed: {
    // 格式化
    _value() {
      if (this.isThousands) return this.$options.filters['_number'](this.value)
      return this.value
    }
  }
};
</script>

vue3 二次封装 el-input

涉及知识点:

  • v-model 语法糖
    • vue3 与 vue2 有些不同,v-model的语法糖由 :modelValue:update:modelValue 组成
  • 计算属性 computed
  • defineProps
  • defineEmits
  • toRefs
  • v-bind="$attrs"
js 复制代码
<template>
  <el-input 
    :modelValue="_modelValue" 
    @update:modelValue="handleInput" 
    @blur="handleBlur" 
    v-bind="$attrs"
  />
</template>

<script lang="ts" setup>
import {  computed, defineProps, defineEmits, toRefs } from 'vue'

type IObject = {
  [props: string]: RegExp
}

const props = defineProps({
  isThousands: {
    type: Boolean,
    default: true
  },
  isInteger: {
    type: Boolean,
    default: false
  },
  isMinus: {
    type: Boolean,
    default: true
  },
  modelValue: {
    type: String,
    default: ''
  }
})

const $emit = defineEmits<{ (event: 'update:modelValue', value: string): void }>()

const { isThousands, isInteger, isMinus, modelValue } = toRefs(props)

const formatNumber = (num: string) => {
  let str = num.toString()
  let flag = false
  if (/^-?\d+(.\d+)?$/.test(str)) {
    if (str.includes('.')) {
      str = str.replace(/\.(\d+)$/, ',$1')
    } else {
      str += ',00'
      flag = true
    }
    while (/\d{4}/.test(str)) {
      str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
    }
    if (flag) {
      str = str.replace(/\,(\d+)$/, '')
    } else {
      str = str.replace(/\,(\d+)$/, '.$1')
    }
  }
  return str
}

const _modelValue = computed(() => {
  if (isThousands.value) return formatNumber(modelValue.value)
  return modelValue.value
})

  // 输入非数字结尾的做特殊处理
const handleBlur = () => {
  if (['.', '-'].includes(modelValue.value)) {
    $emit('update:modelValue', '');
  } else {
    const [integer, decimal] = modelValue.value.split('.')
    let _value = modelValue.value
    if (!decimal /* || decimal.length === 2 && decimal === '00' */) { // 小数不存在或空或.00 ,转为整数
      _value = integer
    } else if (decimal.length === 1) { // 有一位小数,后补0
      _value = `${modelValue.value}0`
    }
    if (_value === '-0') {
      _value = '0'
    }
    $emit('update:modelValue', _value)
  }
}
// 属性控制正则限制输入
const getRegexp = (a: boolean, b: boolean): RegExp => {
  let key = `${!!a}-${!!b}`
  const map: IObject = { // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
    'true-true': /[^0-9\-]/g, // 正负整数
    'true-false': /[^0-9]/g  , // 正整数
    'false-true': /[^0-9\.-]/g, // 正负数(含整数和小数)
    'false-false': /[^0-9\.]/g // 正数(含整数和小数)
  }
  return map[key]
}
const handleInput = (value: string) => {
  const regexp = getRegexp(isInteger.value, isMinus.value)
  const _value = value
  .replace(regexp, '') // 根据属性限制输入
  .replace(/^(-?[^-]*)-*/g, '$1') // 只能开头一个 -
  .replace(/\.{2,}/g, '.') // 只能有一个.
  .replace(/(\.\d{1,2})(.*)$/g, '$1') // 1.12.12312 => 1.12 || .12.32 => .12
  .replace(/^(-?)\./, '$10.') // 以 . 或 -. 开头的 => 0. || -. => -0.
  .replace(/^(-?)0([0-9]+)/, '$1$2') // 正负整数部分不能以0开头

  $emit('update:modelValue', _value)
}
</script>

react 二次封装 antd input

基于antd的input组件封装

js 复制代码
import { Input } from "antd";

const InputMoney = (props) => {
  const {
    isMinus = true,
    isInteger = false,
    isThousands = true,
    value,
    onChange,
    ...rest
  } = props;
  // 格式化千分位
  const formatNumber = (num) => {
    let str = num.toString();
    let flag = false;
    if (/^-?\d+(.\d+)?$/.test(str)) {
      if (str.includes(".")) {
        str = str.replace(/\.(\d+)$/, ",$1");
      } else {
        str += ",00";
        flag = true;
      }
      while (/\d{4}/.test(str)) {
        str = str.replace(/(\d+)(\d{3}\,)/, "$1,$2");
      }
      if (flag) {
        str = str.replace(/\,(\d+)$/, "");
      } else {
        str = str.replace(/\,(\d+)$/, ".$1");
      }
    }
    return str;
  };

  // 输入非数字结尾的做特殊处理
  const handleBlur = () => {
    if ([".", "-"].includes(value)) {
      onChange && onChange("");
    } else {
      const [integer, decimal] = value.split(".");
      let _value = value;
      if (!decimal /* || decimal.length === 2 && decimal === '00' */) {
        // 小数不存在或空或.00 ,转为整数
        _value = integer;
      } else if (decimal.length === 1) {
        // 有一位小数,后补0
        _value = `${value}0`;
      }
      if (_value === "-0") {
        _value = "0";
      }
      onChange && onChange(_value);
    }
  };
  // 属性控制正则限制输入
  const getRegexp = (a, b) => {
    let key = `${!!a}-${!!b}`;
    const map = {
      // 第一个true:isInteger, 第二个true:isMinus,总共四种情况
      "true-true": /[^0-9\-]/g, // 正负整数
      "true-false": /[^0-9]/g, // 正整数
      "false-true": /[^0-9\.-]/g, // 正负数(含整数和小数)
      "false-false": /[^0-9\.]/g, // 正数(含整数和小数)
    };
    return map[key];
  };

  const handleChange = (e) => {
    // console.log(value);
    const { value } = e.target;
    const regexp = getRegexp(isInteger, isMinus);
    const _value = value
      .replace(regexp, "") // 根据属性限制输入
      .replace(/^(-?[^-]*)-*/g, "$1") // 只能开头一个 -
      .replace(/\.{2,}/g, ".") // 只能有一个.
      .replace(/(\.\d{1,2})(.*)$/g, "$1") // 1.12.12312 => 1.12 || .12.32 => .12
      .replace(/^(-?)\./, "$10.") // 以 . 或 -. 开头的 => 0. || -. => -0.
      .replace(/^(-?)0([0-9]+)/, "$1$2"); // 正负整数部分不能以0开头

    onChange && onChange(_value);
  };

  return (
    <Input
      {...rest}
      value={formatNumber(value)}
      onChange={handleChange}
      onBlur={handleBlur}
    />
  );
};

export default InputMoney;

使用自定义组件

vue2、vue3、react使用方式有些许不同

react组件使用

js 复制代码
import { useState } from "react";
import "./App.css";
import InputMoney from "./components/InputMoney";

function App() {
  const [money, setMoney] = useState("");
  return (
    <>
      金额:{money}
      <InputMoney value={money} onChange={setMoney} placeholder="请输入金额" />
    </>
  );
}

export default App;

vue3 组件使用

js 复制代码
<script setup lang="ts">
import { ref} from 'vue';
import inputNumber from '@/components/inputNumber.vue'

let money = ref()

</script>

<template>
  {{ money }}
  <input-number v-model="money" placeholder="请输入金额"/>
</template>

vue2 组件使用

js 复制代码
// import elInputNumber from '@/components/el-input-number'
// 如果多个地方使用,全局注册组件  Vue.component('el-input-number', elInputNumber)
// 否则就局部注册 components: { elInputNumber }

<template>
  <div id="app">
    name值:{{ name }}
    <br />
    <el-input-number style="width: 260px" v-model="name" placeholder="请输入金额"/>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      name: ''
    }
  },
}
</script>

总结

二次封装的组件使用上与使用原有组件库的组件方式一样,区别在于多了几个参数用来限制输入,如果要使用原生input做封装,只要稍微改动input事件的参数即可(e.target.value),由于正则表达式写的并不是很好,大家可以基于此正则上做修改或新增限制。

注意:如果要限制输入长度的话,千分位 , 和 小数点 . 是包含在内的。当输入的末位是小数点的话,上面的格式化千分位会导致计算长度出现bug,即12345. 它不会格式化为 12,345.,则可替换为以下函数:

js 复制代码
function formatNumber() {
    let str = num.toString()
    let flag = false
    if (/^-?\d+(.\d*)?$/.test(str)) {
        if (str.includes('.') && str.split('.')[1]) {
          str = str.replace(/\.(\d+)$/, ',$1')
        } else if (str.includes('.') && !str.split('.')[1]){
          str = str.split('.')[0] + ','
        } else {
          str += ',00'
          flag = true
        }
        while (/\d{4}/.test(str)) {
          str = str.replace(/(\d+)(\d{3}\,)/, '$1,$2')
        }
        if (flag) {
          str = str.replace(/\,(\d+)$/, '')
        } else {
          str = str.replace(/\,(\d*)$/, '.$1')
        }
    }
    return str
}
相关推荐
minDuck几秒前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
嚣张农民2 小时前
推荐3个实用的760°全景框架
前端·vue.js·程序员
落魄小二2 小时前
el-table 表格索引不展示问题
javascript·vue.js·elementui
neter.asia3 小时前
vue中如何关闭eslint检测?
前端·javascript·vue.js
十一吖i3 小时前
前端将后端返回的文件下载到本地
vue.js·elementplus
光影少年3 小时前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
Rattenking3 小时前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js