第八集: 从零开始实现(输入框input,textarea组件)
本集定位:
input组件是交互的一大利器, 他与用户的交流最为密切, 所以奠定了他在组件界的重要地位.
textarea也算是一种input, 如果可以的话, 本集也会一起说完, 毕竟是一个类型的, 一起学完收获会很大.
古人云:"组件不封输入框,一到面试就发慌"
一. v-model 简介
大家如果对 v-model这个指令的原理不熟悉, 建议去学习下vue源码或者看看相关的分析文章, 很重要的知识, 封装组件多了就会知道这个指令真是太棒了! 这里我就简单说一下他的规则.
1: 父级在组件上绑定了v-model时, 其实就是在往组件里面传递value变量.
2: 你的组件在props上定义value, 就可以取到值.
3: 每当组件里this.$emit("input",n)往外面发送事件的时候, 外面会把这个n值 赋值给value
4: 这么设计的原因: 你在组件里面无权改变传入的值, 这个值你想改成什么值就要吐出去, 让外面改.
好了说了这么多开始实战吧!
二. 基本结构
vue-cc-ui/src/components/Input/index.js
老套路, 统一导出为了适配vue.use的使用方式
import Input from './main/input.vue'
Input.install = function(Vue) {
Vue.component(Input.name, Input);
};
export default Input
vue-cc-ui/src/components/Input/main/input.vue
- type: 这个属性比较重要, 因为要通过它来区分input与textarea, 还可以为input指定number模式.
- 命名依然是bem
- v-bind="$attrs" 解释下这个的意思, $attrs指的就是用户传进来的属性, 但是不包括我们组件内部用props接收的属性, 也不包括class style这种, 写它是为了用户可以传很多input原生的属性, 毕竟我们没必要把所有属性都做处理, 让组件保有原生功能.
- placeholder这种模式基本也被现代抛弃了, 针对他也可以封装成一个具体的组件, 这个属性想调整属性实在是太困难了, 更别说我们现在还需要placeholder轮播,变色,点击等等效果.
- vue 在行间写事件的时候, 事件对象会以$event的形式传给你使用, 其实从代码的角度来说, 是监控到你这里用了$event关键词,则把对应的参数赋值为事件对象.
<template>
<div class="cc-input">
<input type="text"
class='cc-input__inner'
:value="value"
v-bind="$attrs"
:placeholder="placeholder"
@input="$emit('input',$event.target.value)"/>
</div>
</template>
props: {
value: [String, Number],
placeholder: [String, Number],
type: {
type: String,
default: "text"
}
},
三. 丰富事件
- 输入框有很多种事件, 他们能给用户更好的体验性.
- 比如在手机端, 我们项目之前遇到的问题就是, 用户点击输入框的时候, 会弹出手机键盘, 但是弹出的键盘会把输入框顶上去, 某些型号的手机会出现, 就算输入完毕点击完成, 可是输入框还是被顶上去的状态, 后来我是借助blur 与 focus事件才兼容了这写手机
- 很多输入框也采取节流与防抖, 比如做搜索的相关模糊匹配
- 有些以搜索为主的页面, 需要自动聚焦
<input :type="type"
class='cc-input__inner'
:value="value"
v-bind="$attrs"
:autofocus="autofocus" // 是否自动聚焦
:placeholder="placeholder"
@blur="blur($event.target.value)"
@input="$emit('input',$event.target.value)"
// 这里有个小细节, 就是这个事件绑定了两个操作
// 不仅触发聚焦事件, 还把变量focus设定为真
@focus="$emit('focus',$event.target.value);focus=true"
@change="$emit('change',$event.target.value)" />
四. 各种状态
- 禁用状态, 置灰并且把鼠标变为禁止状态 (disabled)
- 只读, 并不置灰, 但是也不能改 (readonly)
具体样式会在后面出来详细解释
<input :type="type"
:disabled="disabled" // 都是原生属性, 但要添加样式
:readonly="readonly" // 都是原生属性, 不用添加样式
:class="{ 'cc-input--input__disabled':disabled }" />
五. 为输入框添加状态, 并附上icon选项
- 很多输入框左右都要放个icon充充门面, 分为左侧与右侧icon
- 右侧icon允许输入文字, icon要有相应的点击效果
- 当组件为disabled状态的时候, icon也要相应的置灰
<template>
<div class="cc-input"
:class="{
// 对每种状态给与相应的class
'cc-input__error':error,
'cc-input__normal':!disabled&&!normal,
'cc-input__abnormal':normal,
'cc-input__disabled':disabled,
}"
:style="{
// 输入框有悬停放大的效果, 这里可以调节放大的角度, 下面有图演示
'transform-origin':`${transformOrigin} 0`
}">
<nav v-if="leftIcon"
class="cc-input__prefix is-left"
// 返回相应的点击事件
@click="$emit('clickLeftIcon')">
<ccIcon :name='leftIcon'
:color='iconColor'
// 这里图标也要置灰
:disabled='disabled' />
</nav>
<input :type="type"
class='cc-input__inner'
:value="value"
v-bind="$attrs"
:disabled="disabled"
:readonly="readonly"
:autofocus="autofocus"
:placeholder="placeholder"
:class="{ 'cc-input--input__disabled':disabled }"
@blur="blur($event.target.value)"
@input="$emit('input',$event.target.value)"
@focus="$emit('focus',$event.target.value);focus=true"
@change="$emit('change',$event.target.value)" />
<nav v-if="icon&&!clear"
class="cc-input__prefix is-right"
@click="$emit('clickRightIcon')">
<ccIcon :name="clear?'cc-close':icon"
:color='iconColor'
:disabled='disabled' />
// 允许用户插入各种节点
<slot />
</nav>
</div>
</template>
效果图
六. 清空按钮
现在的输入框基本都有这个清空按钮, 毕竟可以节省用的时间, 也算是个好功能,
当用户传入clear的时候会判断, 是否禁止修改, 框内是否有值, 是否是hover状态
hover事件放在父级上
<div class="cc-input"
@mouseenter="hovering = true"
@mouseleave="hovering = false">
<nav v-if="showClear"
class="cc-input__clear"
@click="clickClear">
<ccIcon name="cc-close"
:disabled='disabled' />
// 这里是为了样式的统一
// 比如用户在右侧按钮写了很多文字
// 那么clear按钮不好定位, 所以才写了这个站位
<span style=" opacity: 0;">
<slot />
</span>
</nav>
清除事件, 对外返回空就ok
clickClear() {
this.$emit("input", "");
this.$emit("change", "");
},
判断是否显示
computed: {
showClear() {
if (
this.clear && // 开启功能
!this.disabled && // 不是禁用
!this.readonly && // 不是只读
this.value!== '' && // 不是空值
(this.hovering || this.focus) // 聚焦或者hover状态下
)return true;
return false;
}
},
vue-cc-ui/src/style/Input.scss
// 引入老四样
@import './common/var.scss';
@import './common/extend.scss';
@import './common/mixin.scss';
@import './config/index.scss';
// 这里毕竟是两个月前写的组件, 命名方面不是很好, 接下来会统一改正
@include b(input) {
cursor: pointer;
position: relative;
align-items: center;
display: inline-flex; // 直接flex会独占一行
background-color: white;
transition: all .3s;
@include b(input__inner) {
border: none;
flex: 1;
width: 100%;
font-size: 1em;
padding: 9px 16px;
&:focus { outline: 0; } // 这样写对障碍阅读不是很友好
@include placeholder{ // placeholder设置颜色很头疼, 请看下面
color: $--color-input-placeholder;
}
};
@include b(input__prefix) {
align-items: center;
display: inline-flex;
&:hover{transform: scale(1.1)}
@include when(left) {
padding-left:6px;
}
@include when(right) {
padding-right:6px;
}
};
@include b(input__clear){
position: absolute;
right: 24px;
&:hover{ animation: size .5s infinite linear;}
};
@include b(input--input__disabled){
@include commonShadow(disabled);
};
@at-root {
@include b(input__normal){
@include commonShadow($--color-black);
&:hover {
z-index: 6;
transform: scale(1.2);
}
}
@include b(input__error){
@include commonShadow(danger);
}
@include b(input__abnormal){
@include commonShadow($--color-black);
}
}
}
element 这个处理做的也不错
@mixin placeholder {
&::-webkit-input-placeholder {
@content;
}
&::-moz-placeholder {
@content;
}
&:-ms-input-placeholder {
@content;
}
}
七. textarea 文本域
基本结构
- 在用户type输入的是textarea时候开启
- 把上面的基础功能复制下来, 直接放上就可以用的
- textareaCalcStyle: 来设置他的宽高, 毕竟他与input不同, 可能需要很大面积
- 用户可以设置最大高度与最小高度
- 难点: 如果用户选择了自动适应高度那就麻烦了, 这个组件没有提供原生的解决方案, 第一版我是采用获取其高度进行运算得出来的, 但是及特殊的情况会有bug, 最后参考了element-ui的实现方式, 这里也让我学习到了.
<template>
<div class="cc-input" ....>
<template v-if="type !== 'textarea'">
<input :type="type" ..../>
</template>
<textarea v-else
// 必须获取这个dom
ref="textarea"
class='cc-input__inner'
:value="value"
v-bind="$attrs"
:disabled="disabled"
:readonly="readonly"
:autofocus="autofocus"
:placeholder="placeholder"
@blur="$emit('blur',$event.target.value)"
@input="$emit('input',$event.target.value)"
@focus="$emit('focus',$event.target.value)"
@change="$emit('change',$event.target.value)"
:style="{
width:rows,
height:cols,
...textareaCalcStyle}"
:class="{
'cc-input--input__disabled':disabled,
'cc-input--input__autosize':autosize}" />
</div>
</template>
针对textarea获取其真实高度进行高度的动态赋值;
我来说说他的原理, 制作一个与textarea对象相同的元素, 获取他的滚动距离与高度, 计算出总的高度, 然后赋值给真正的textarea, 这里的亮点就是怎么做一个相同的dom, 因为用户可能给这个dom不同的样式, 不同的class, 各种各样的父级, 腹肌还会影响这个元素的样式;
// 个人建议, 这种生命周期函数都放在最底部, 并且要保持单一职责
mounted() {
this.$nextTick(this.resizeTextarea);
}
1: 判断是不是 autosize自动高度, 并且是组件autosize
2: 用户是否设置了最大高度与最小高度的限制
3: 这个函数只负责处理是否进行计算 calcTextareaHeight 负责计算.
resizeTextarea() {
const { autosize, type } = this;
if (type !== "autosize" || !autosize) return;
const minRows = autosize.min;
const maxRows = autosize.max;
this.textareaCalcStyle = this.calcTextareaHeight(
this.$refs.textarea,
minRows,
maxRows
);
},
calcTextareaHeight
calcTextareaHeight(el, min, max) {
// 也算是单例模式, 制作一个元素就行了
if (!window.hiddenTextarea) {
window.hiddenTextarea = document.createElement("textarea");
document.body.appendChild(window.hiddenTextarea);
}
// 取得他的属性, 具体获取属性函数下面会讲
let [boxSizing, paddingSize, borderSize] = this.calculateNodeStyling(el);
// 滚动距离
let height = window.hiddenTextarea.scrollHeight;
// 是否是怪异盒模型, 进行分别的计算
if (boxSizing === "border-box") {
height = height + borderSize;
} else {
height = height - paddingSize;
}
// 及时清理,让用户看不到这个元素
window.hiddenTextarea.parentNode &&
window.hiddenTextarea.parentNode.removeChild(window.hiddenTextarea);
window.hiddenTextarea = null;
if (min && height < min) height = min;
else if (max && height > max) height = max;
return { height: height + "px" };
}
calculateNodeStyling
calculateNodeStyling(el) {
// 模拟元素通过值的输入模拟真正的元素
window.hiddenTextarea.value = this.value;
const style = window.getComputedStyle(el);
const boxSizing = style.getPropertyValue("box-sizing");
const paddingTop = style.getPropertyValue("padding-top");
const paddingBottom = style.getPropertyValue("padding-bottom");
const borderTopWidth = style.getPropertyValue("border-top-width");
const borderBottomWidth = style.getPropertyValue("border-bottom-width");
const contextStyle = this.CONTEXT_STYLE.map(
name => `${name}:${style.getPropertyValue(name)}`
).join(";");
window.hiddenTextarea.setAttribute(
"style",
`${contextStyle};${this.HIDDEN_STYLE}`
);
return [
boxSizing,
parseInt(paddingBottom) + parseInt(paddingTop),
parseInt(borderBottomWidth) + parseInt(borderTopWidth)
];
},
上面 用到的this.CONTEXT_STYLE数据是样式的列表
data() {
return {
focus: false, // 监听输入框的聚焦失焦
hovering: false,
textareaCalcStyle: {},
CONTEXT_STYLE: [
"width",
"font-size",
"box-sizing",
"line-height",
"padding-top",
"font-family",
"font-weight",
"text-indent",
"border-width",
"padding-left",
"padding-right",
"letter-spacing",
"padding-bottom",
"text-rendering",
"text-transform"
]
};
},
至此才把这个组件做完, 好辛苦
end
如果想做到面面俱到就没有简单的组件, element上的每个组件都值得借鉴.
其实很多原理明白之后学习才能更快捷, 最近拿出时间与大家风向一下vue的实现原理, vue-router vuex等等的实现原理, 希望能对大家对我自己都有帮助吧,, 只能说学海无涯回头是岸?.
希望大家一起进步, 实现自我价值!!
下一集准备聊聊 计数器
更多好玩的效果请关注个人博客: 链接描述
github: 链接描述
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。