Element UI 源码改造 —— 自定义数字输入框的实现

简介: Element UI 源码改造 —— 自定义数字输入框的实现

需求描述

表单中需要一个数字输入框,功能与 el-input-number 基本一样,但有以下区别:

1. 按上下方向键时,不改变输入的内容

2. 输入的值超出最大值或小于最小值时,保留输入的值,而不是当失去焦点时自动替换为最大或最小值

3. 像el-input一样,可以配置可清空按钮

实现方案

复制一份Element UI源码中的el-input-number对应的文件,改造成满足需求的组件。

完整代码范例

1. 找到 Element UI源码中的el-input-number对应的文件

node_modules/element-ui/packages/input-number/src/input-number.vue

2. 新建文件 newElNumberInput.vue

将源码input-number.vue 拷贝到 newElNumberInput.vue中,并按需求对源码进行修改,最终代码如下:

<template>
    <div
            @dragstart.prevent
            :class="[
      'el-input-number',
      inputNumberSize ? 'el-input-number--' + inputNumberSize : '',
      { 'is-disabled': inputNumberDisabled },
      { 'is-without-controls': !controls },
      { 'is-controls-right': controlsAtRight }
    ]">
    <span
            class="el-input-number__decrease"
            role="button"
            v-if="controls"
            v-repeat-click="decrease"
            :class="{'is-disabled': minDisabled}"
            @keydown.enter="decrease">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i>
    </span>
        <span
                class="el-input-number__increase"
                role="button"
                v-if="controls"
                v-repeat-click="increase"
                :class="{'is-disabled': maxDisabled}"
                @keydown.enter="increase">
      <i :class="`el-icon-${controlsAtRight ? 'arrow-up' : 'plus'}`"></i>
    </span>
        <el-input
                ref="input"
                :clearable = 'clearable'
                :value="displayValue"
                :placeholder="placeholder"
                :disabled="inputNumberDisabled"
                :size="inputNumberSize"
                :max="max"
                :min="min"
                :name="name"
                :label="label"
                @blur="handleBlur"
                @focus="handleFocus"
                @input="handleInput"
                @change="handleInputChange">
        </el-input>
    </div>
</template>
<script>
    import ElInput from 'element-ui/packages/input';
    import Focus from 'element-ui/src/mixins/focus';
    import RepeatClick from 'element-ui/src/directives/repeat-click';
 
    export default {
        name: 'ElInputNumber',
        mixins: [Focus('input')],
        inject: {
            elForm: {
                default: ''
            },
            elFormItem: {
                default: ''
            }
        },
        directives: {
            repeatClick: RepeatClick
        },
        components: {
            ElInput
        },
        props: {
            clearable:Boolean,
            step: {
                type: Number,
                default: 1
            },
            stepStrictly: {
                type: Boolean,
                default: false
            },
            max: {
                type: Number,
                default: Infinity
            },
            min: {
                type: Number,
                default: -Infinity
            },
            value: {},
            disabled: Boolean,
            size: String,
            controls: {
                type: Boolean,
                default: true
            },
            controlsPosition: {
                type: String,
                default: ''
            },
            name: String,
            label: String,
            placeholder: String,
            precision: {
                type: Number,
                validator(val) {
                    return val >= 0 && val === parseInt(val, 10);
                }
            }
        },
        data() {
            return {
                currentValue: 0,
                userInput: null
            };
        },
        watch: {
            value: {
                immediate: true,
                handler(value) {
                    let newVal = value === undefined ? value : Number(value);
                    if (newVal !== undefined) {
                        if (isNaN(newVal)) {
                            return;
                        }
                        if (this.stepStrictly) {
                            const stepPrecision = this.getPrecision(this.step);
                            const precisionFactor = Math.pow(10, stepPrecision);
                            newVal = Math.round(newVal / this.step) * precisionFactor * this.step / precisionFactor;
                        }
                        if (this.precision !== undefined) {
                            newVal = this.toPrecision(newVal, this.precision);
                        }
                    }
                    // if (newVal >= this.max) newVal = this.max;
                    // if (newVal <= this.min) newVal = this.min;
                    this.currentValue = newVal;
                    this.userInput = null;
                    this.$emit('input', newVal);
                }
            }
        },
        computed: {
            minDisabled() {
                return this._decrease(this.value, this.step) < this.min;
            },
            maxDisabled() {
                return this._increase(this.value, this.step) > this.max;
            },
            numPrecision() {
                const {value, step, getPrecision, precision} = this;
                const stepPrecision = getPrecision(step);
                if (precision !== undefined) {
                    if (stepPrecision > precision) {
                        console.warn('[Element Warn][InputNumber]precision should not be less than the decimal places of step');
                    }
                    return precision;
                } else {
                    return Math.max(getPrecision(value), stepPrecision);
                }
            },
            controlsAtRight() {
                return this.controls && this.controlsPosition === 'right';
            },
            _elFormItemSize() {
                return (this.elFormItem || {}).elFormItemSize;
            },
            inputNumberSize() {
                return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
            },
            inputNumberDisabled() {
                return this.disabled || (this.elForm || {}).disabled;
            },
            displayValue() {
                if (this.userInput !== null) {
                    return this.userInput;
                }
                let currentValue = this.currentValue;
                if (typeof currentValue === 'number') {
                    if (this.stepStrictly) {
                        const stepPrecision = this.getPrecision(this.step);
                        const precisionFactor = Math.pow(10, stepPrecision);
                        currentValue = Math.round(currentValue / this.step) * precisionFactor * this.step / precisionFactor;
                    }
                    if (this.precision !== undefined) {
                        currentValue = currentValue.toFixed(this.precision);
                    }
                }
                return currentValue;
            }
        },
        methods: {
            toPrecision(num, precision) {
                if (precision === undefined) precision = this.numPrecision;
                return parseFloat(Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision));
            },
            getPrecision(value) {
                if (value === undefined) return 0;
                const valueString = value.toString();
                const dotPosition = valueString.indexOf('.');
                let precision = 0;
                if (dotPosition !== -1) {
                    precision = valueString.length - dotPosition - 1;
                }
                return precision;
            },
            _increase(val, step) {
                if (typeof val !== 'number' && val !== undefined) return this.currentValue;
                const precisionFactor = Math.pow(10, this.numPrecision);
                // Solve the accuracy problem of JS decimal calculation by converting the value to integer.
                return this.toPrecision((precisionFactor * val + precisionFactor * step) / precisionFactor);
            },
            _decrease(val, step) {
                if (typeof val !== 'number' && val !== undefined) return this.currentValue;
                const precisionFactor = Math.pow(10, this.numPrecision);
                return this.toPrecision((precisionFactor * val - precisionFactor * step) / precisionFactor);
            },
            increase() {
                if (this.inputNumberDisabled || this.maxDisabled) return;
                const value = this.value || 0;
                const newVal = this._increase(value, this.step);
                this.setCurrentValue(newVal);
            },
            decrease() {
                if (this.inputNumberDisabled || this.minDisabled) return;
                const value = this.value || 0;
                const newVal = this._decrease(value, this.step);
                this.setCurrentValue(newVal);
            },
            handleBlur(event) {
                this.$emit('blur', event);
            },
            handleFocus(event) {
                this.$emit('focus', event);
            },
            setCurrentValue(newVal) {
                const oldVal = this.currentValue;
                if (typeof newVal === 'number' && this.precision !== undefined) {
                    newVal = this.toPrecision(newVal, this.precision);
                }
                // if (newVal >= this.max) newVal = this.max;
                // if (newVal <= this.min) newVal = this.min;
                if (oldVal === newVal) return;
                this.userInput = null;
                this.$emit('input', newVal);
                this.$emit('change', newVal, oldVal);
                this.currentValue = newVal;
            },
            handleInput(value) {
                this.userInput = value;
            },
            handleInputChange(value) {
                const newVal = value === '' ? undefined : Number(value);
                if (!isNaN(newVal) || value === '') {
                    this.setCurrentValue(newVal);
                } else {
                    this.setCurrentValue(undefined)
                }
                this.userInput = null;
            },
            select() {
                this.$refs.input.select();
            }
        },
        mounted() {
            let innerInput = this.$refs.input.$refs.input;
            innerInput.setAttribute('role', 'spinbutton');
            innerInput.setAttribute('aria-valuemax', this.max);
            innerInput.setAttribute('aria-valuemin', this.min);
            innerInput.setAttribute('aria-valuenow', this.currentValue);
            innerInput.setAttribute('aria-disabled', this.inputNumberDisabled);
        },
        updated() {
            if (!this.$refs || !this.$refs.input) return;
            const innerInput = this.$refs.input.$refs.input;
            innerInput.setAttribute('aria-valuenow', this.currentValue);
        }
    };
</script>


3. 在目标文件中引入使用

<template>
    <div style="padding: 30px">
        <el-form ref="formRef" :model="formData" size="mini" label-width="100px">
            <el-form-item label="分数" prop='score'
                          :rules="[
                           { required: true, message: '不能为空', trigger: 'blur' },
                           {type: 'number', min:0,max:99,message: '请输入0-99之间的数字', trigger: 'blur' }
                            ]"
            >
                <NewElNumberInput clearable :controls="false" :max="99" v-model.number="formData.score"/>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" @click="submit">提交</el-button>
            </el-form-item>
        </el-form>
    </div>
</template>
<script>
    import NewElNumberInput from './newElNumberInput'
 
    export default {
        components: {NewElNumberInput},
        data() {
            return {
                formData: {}
            }
        },
        methods: {
            submit() {
                this.$refs.formRef.validate((valid) => {
                    if (valid) {
                        alert('通过表单校验!');
                    } else {
                        console.log('表单校验失败!');
                        return false;
                    }
                });
            }
        }
    }
</script>
<style scoped>
</style>

4. 最终效果

友情提示

若有其他通过改造 Element UI 源码即可快速实现的功能,都可以参考本例实现。

目录
相关文章
|
1天前
「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
本篇将带你实现一个自定义天气预报组件。用户可以通过选择不同城市来获取相应的天气信息,页面会显示当前城市的天气图标、温度及天气描述。这一功能适合用于动态展示天气信息的小型应用。
60 38
「Mac畅玩鸿蒙与硬件46」UI互动应用篇23 - 自定义天气预报组件
|
28天前
|
前端开发 搜索推荐 开发者
「Mac畅玩鸿蒙与硬件20」鸿蒙UI组件篇10 - Canvas 组件自定义绘图
Canvas 组件在鸿蒙应用中用于绘制自定义图形,提供丰富的绘制功能和灵活的定制能力。通过 Canvas,可以创建矩形、圆形、路径、文本等基础图形,为鸿蒙应用增添个性化的视觉效果。本篇将介绍 Canvas 组件的基础操作,涵盖绘制矩形、圆形、路径和文本的实例。
63 12
「Mac畅玩鸿蒙与硬件20」鸿蒙UI组件篇10 - Canvas 组件自定义绘图
|
28天前
|
搜索推荐 前端开发 开发者
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
自定义动画让开发者可以设计更加个性化和复杂的动画效果,适合表现独特的界面元素。鸿蒙提供了丰富的工具,支持通过自定义路径和时间控制来创建复杂的动画运动。本篇将带你学习如何通过自定义动画实现更多样化的效果。
72 11
「Mac畅玩鸿蒙与硬件19」鸿蒙UI组件篇9 - 自定义动画实现
|
24天前
|
UED
「Mac畅玩鸿蒙与硬件31」UI互动应用篇8 - 自定义评分星级组件
本篇将带你实现一个自定义评分星级组件,用户可以通过点击星星进行评分,并实时显示评分结果。为了让界面更具吸引力,我们还将添加一只小猫图片作为评分的背景装饰。
63 6
「Mac畅玩鸿蒙与硬件31」UI互动应用篇8 - 自定义评分星级组件
|
26天前
|
前端开发 开发者
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
自定义组件可以帮助开发者实现复用性强、逻辑清晰的界面模块。通过自定义组件,鸿蒙应用能够提高代码的可维护性,并简化复杂布局的构建。本篇将介绍如何创建自定义组件,如何向组件传递数据,以及如何在不同页面间复用这些组件。
36 5
「Mac畅玩鸿蒙与硬件23」鸿蒙UI组件篇13 - 自定义组件的创建与使用
|
18天前
|
前端开发 数据安全/隐私保护
全新紫色新UI数码盲盒系统源码/全开源无加密/附教程
全新紫色新UI数码盲盒系统源码/全开源无加密/附教程 前端uniapp+H5 后端FastAdmin框架 可打包成APP多端运行 亲测可用
41 13
|
2月前
|
API UED 容器
深入探索 Element UI:自定义滚动条与弹出层管理的技巧
在这篇博客中,我们将深入探讨 Element UI 中的自定义滚动条及弹出层管理技巧。文章详细介绍了 el-scrollbar 组件的使用和参数设置,以及 PopupManager 如何有效管理弹出层的 z-index。我们还将探讨如何实现灵活的全屏组件,利用 vue-popper 创建自定义弹出层,最后介绍 ClickOutside 指令的用法。这些高级技巧将帮助你提升 Element UI 应用程序的用户体验与交互灵活性。
287 1
深入探索 Element UI:自定义滚动条与弹出层管理的技巧
|
2月前
|
JavaScript 索引
Vue开发中Element UI/Plus使用指南:常见问题(如Missing required prop: “value“)及中文全局组件配置解决方案
Vue开发中Element UI/Plus使用指南:常见问题(如Missing required prop: “value“)及中文全局组件配置解决方案
169 0
|
4月前
|
开发者 C# Android开发
明白吗?Xamarin与Native的终极对决:究竟哪种开发方式更适合您的项目需求,让我们一探究竟!
【8月更文挑战第31天】随着移动应用开发的普及,开发者面临多种技术选择。本文对比了跨平台解决方案Xamarin与原生开发方式的优势与劣势。Xamarin使用C#进行跨平台开发,代码复用率高,可大幅降低开发成本;但因基于抽象层,可能影响性能。原生开发则充分利用平台特性,提供最佳用户体验,但需维护多套代码库,增加工作量。开发者应根据项目需求、团队技能和预算综合考量,选择最适合的开发方式。
130 0
|
1月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
下一篇
DataWorks