需求:在实际项目中,我们有很多需要用户去输入数字的输入框,比如输入经度、纬度、或数量输入框等。封装一个数字输入框组件,不仅可以满足大部分场景,也可以提供维护性、可扩展性,废话不多说,直接上代码
代码解读
xml
<!--
核心部分:
1. v-bind="$attrs" 将所有传入的属性透传给 el-input
2. autocomplete="new-password" 禁用浏览器自动填充功能
3. 插槽部分通过 v-for 遍历所有插槽并透传给 el-input
-->
<!--
什么是 $attrs:
$attrs 是 Vue 实例的一个属性,它包含了父组件传递给子组件的所有非 prop 属性。换句话说,就是父组件传递给子组件的属性中,那些没有在子组件的 props 中声明的属性。
v-bind="$attrs" 的作用:
将父组件传递给当前组件的所有非 prop 属性,全部绑定到当前组件模板中的某个元素上。
-->
<!-- 实际使用示例 -->
<!-- 假设你在其他组件中这样使用 NumberInput: -->
<template>
<NumberInput
v-model="price"
placeholder="请输入价格"
class="price-input"
style="width: 200px"
disabled
/>
</template>
<!--
在这个例子中:
1. v-model 是 NumberInput 组件声明的 prop,会被正确处理
2. placeholder、class、style、disabled 这些属性没有在 NumberInput 的 props 中声明
如果没有 v-bind="$attrs",这些属性将不会应用到实际的输入框上。但因为有了 v-bind="$attrs",这些属性会被传递并绑定到 el-input 练级上,最终渲染为:
-->
<input placeholder="请输入价格" class="price-input" style="width: 200px" disabled />
<!-- 动态插槽透传 -->
<template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]>
<slot :name="key"></slot>
</template>
<!--
这段代码的作用是动态插槽透传,即将父组件传递给当前组件的所有插槽内容,原封不动地传递给内部的 el-input 组件。
1. 遍历 $slots 对象,获取所有传递给当前组件的插槽
2. key 是插槽的名称(如 "prepend"、"append" 等)
3. index 是遍历的索引,用作 :key 的值
4. v-slot:[key] 是一个动态插槽,用于将父组件传递给当前组件的插槽内容,原封不动地传递给内部的 el-input 插槽。
5. <slot :name="key"></slot>在每个动态插槽中,使用同名的插槽内容进行渲染,实现插槽内容的透传
-->
<!-- 实际应用场景 -->
<!-- 假设你在使用 NumberInput 组件时传递了插槽: -->
<template>
<NumberInput v-model="price">
<template #prepend>
<span>¥</span>
</template>
<template #append>
<span>元</span>
</template>
</NumberInput>
</template>
<!-- 由于 NumberInput 组件中的这段代码,这些插槽内容会被透传给内部的 el-input 组件,最终渲染效果相当于: -->
<el-input v-model="price">
<template #prepend>
<span>¥</span>
</template>
<template #append>
<span>元</span>
</template>
</el-input>
<!-- 为什么这样做? -->
<!--
1. 组件封装: NumberInput 是对 el-input 的封装,希望保留 el-input 的所有插槽功能
2. 减少重复工作: 不需要手动声明和处理 el-input 的每一个插槽
3. 提高可维护性: 当 el-input 添加新插槽时,无需修改 NumberInput 组件代码
4. 灵活性: 父组件可以像直接使用 el-input 一样使用 NumberInput 的插槽
-->
<!-- 实际使用示例 -->
<template>
<!-- 基本用法 -->
<NumberInput v-model="price" />
<!-- 只允许整数,最大3位 -->
<NumberInput v-model="quantity" :only-int="true" :int-max-length="3" />
<!-- 允许小数,整数部分最多5位,小数部分最多2位 -->
<NumberInput v-model="amount" :int-max-length="5" :decimal-max-length="2" />
</template>
<!-- 整个流程机制详解 -->
<!-- 1.初始化时:-->
<!--
1.1 父组件的 price 值为 '123.45'
1.2 通过 v-model 传递给 NumberInput 的 modelValue prop
1.3 NumberInput 内部 value 被初始化为 '123.45'
1.4 由于设置了 immediate: true,watch 立即执行,将 value.value 设置为 '123.45'
-->
<!-- 2. 用户输入时: -->
<!--
2.1 用户在输入框中输入字符
2.2 触发 @input="onInput" 事件
2.3 onInput 函数处理输入内容(过滤非法字符、长度限制等)
2.4 处理后的值赋给 value.value
2.5 watch(value, ...) 监听到变化,触发 emit
("update:modelValue", value.value)
2.6 父组件接收到 update:modelValue 事件,更新 price 的值
-->
<!-- 3. 父组件更新时: -->
<!--
3.1 如果父组件以其他方式改变了 price 的值
3.2 通过 v-model 传递给 NumberInput 的 modelValue prop
3.3 watch(() => props.modelValue, ...) 监听到变化
3.4 将新的值同步到内部的 value.value
3.5 由于使用了 v-model.trim="value",输入框显示更新后的值
-->
<!-- const emit = defineEmits(["update:modelValue", "input"]) 实现原理 -->
<!--
1. 这是 Vue 3 Composition API 提供的编译器宏(compiler macro)
2. 用于声明组件可以触发哪些自定义事件
3. 返回一个 emit 函数,用于实际触发事件
4. update:modelValue:用于实现 v-model 双向绑定
5. input:用于通知父组件发生了输入事件
-->
<!-- 实际使用场景 -->
<!-- 1. update:modelValue 事件 -->
<!--
1.1 在 watch 监听内部 value 变化时触发
watch(value, () => {
emit("update:modelValue", value.value);
});
当组件内部的值发生变化时,通过触发 update:modelValue 事件通知父组件更新绑定的值
父组件使用示例:
<template>
<!- v-model 会监听 update:modelValue 事件来更新 price ->
<NumberInput v-model="price" />
</template>
-->
<!-- 2. input 事件 -->
<!--
2.1 用于通知父组件发生了输入事件:
const onInput = (text: string) => {
// ... 处理输入逻辑
value.value = val;
emit("input"); // 通知父组件发生输入
};
父组件可以监听这个事件:
<template>
<NumberInput
v-model="price"
@input="handleInput"
/>
</template>
-->
子组件
ini
<template>
<el-input
ref="inputRef"
v-bind="$attrs"
v-model.trim="value"
@input="onInput"
autocomplete="new-password"
>
<template v-for="(item, key, index) in $slots" :key="index" v-slot:[key]>
<slot :name="key"></slot>
</template>
</el-input>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const props = defineProps<{
// 1. 定义 props,接收父组件传入的 modelValue
modelValue?: string | number;
onlyInt?: boolean; // 是否只允许整数 true 时:仅允许整数 false 时:允许小数
intMaxLength?: number; // 整数部分最大长度
decimalMaxLength?: number; // 小数部分最大长度
}>();
// 2. 定义事件发射器
const emit = defineEmits(["update:modelValue", "input"]);
// 3. 创建内部响应式值,初始化为 props.modelValue
const value = ref(props.modelValue);
// 4. 监听外部传入props.modelValue 值变化,同步到内部 value
watch(
() => props.modelValue,
(val: any) => {
value.value = val;
},
{
immediate: true, // 立即执行一次
}
);
// 5. 监听内部 value 变化,通知父组件
watch(value, () => {
emit("update:modelValue", value.value);
});
// 6. 处理用户输入处理函数
const onInput = (text: string) => {
let val = "";
if (props.onlyInt) {
// 只允许整数
val = text.replace(/[^0-9]/g, "");
if (props.intMaxLength) {
val = val.slice(0, props.intMaxLength);
}
} else {
// 允许小数
val = text.replace(/[^0-9.]/g, "");
const parts = val.split(".");
if (parts.length == 1) {
// 没有小数点
if (props.intMaxLength) {
val = val.slice(0, props.intMaxLength);
}
} else {
// 有小数点
let [ipart, dpart] = parts;
if (props.intMaxLength) {
// 整数部分限制
ipart = ipart.slice(0, props.intMaxLength);
}
if (props.decimalMaxLength) {
// 小数部分限制
dpart = dpart.slice(0, props.decimalMaxLength);
}
val = `${ipart}.${dpart}`;
}
}
value.value = val;
emit("input");
};
// 获取 el-input 实例引用
const inputRef = ref();
// 暴露focus方法给父组件调用
const focus = () => {
inputRef.value.focus();
};
// 暴露blur方法给父组件调用
const blur = () => {
inputRef.value.blur();
};
// 暴露clear方法给父组件调用
const clear = () => {
inputRef.value.clear();
};
defineExpose({
focus,
blur,
clear,
});
</script>
<style lang="scss"></style>
父组件
ini
<NumberInput
:disabled="disabled"
v-model.trim="lng"
placeholder="请输入经度"
class="w150"
/>
<NumberInput
:disabled="disabled"
v-model.trim="lat"
placeholder="请输入纬度"
class="w150 ml10"
/>
import NumberInput from "@/components/NumberInput/Index.vue";
END...