Element分析(组件篇)——Input

input组件相对来说复杂一点,我们先从它用到的一个工具库calcTextareaHeight.js进行分析。


calcTextareaHeight.js

calcTextareaHeight.js使用来计算文本框的高度的,我们根据代码顺序从上往下进行分析。

HIDDEN_STYLE

HIDDEN_STYLE是一个常量,存储隐藏时候的css样式的。

const HIDDEN_STYLE = `
  height:0 !important;
  visibility:hidden !important;
  overflow:hidden !important;
  position:absolute !important;
  z-index:-1000 !important;
  top:0 !important;
  right:0 !important
`;

CONTEXT_STYLE

CONTEXT_STYLE也是一个常量,用来存储要查询的样式名。

const CONTEXT_STYLE = [
  'letter-spacing',
  'line-height',
  'padding-top',
  'padding-bottom',
  'font-family',
  'font-weight',
  'font-size',
  'text-rendering',
  'text-transform',
  'width',
  'text-indent',
  'padding-left',
  'padding-right',
  'border-width',
  'box-sizing'
];

calculateNodeStyling

calculateNodeStyling用来获取结点的某些样式。

function calculateNodeStyling(node) {
  const style = window.getComputedStyle(node);  // 获取结点的计算后的样式,即实际渲染的样式

  const boxSizing = style.getPropertyValue('box-sizing');  // 获取 box-sizing 的值

  // 上下的 padding 之和
  const paddingSize = (
    parseFloat(style.getPropertyValue('padding-bottom')) +
    parseFloat(style.getPropertyValue('padding-top'))
  );

  // 上下的边框宽度和(其实是看上去的高度)
  const borderSize = (
    parseFloat(style.getPropertyValue('border-bottom-width')) +
    parseFloat(style.getPropertyValue('border-top-width'))
  );

  // 其他一些样式
  const contextStyle = CONTEXT_STYLE
    .map(name => `${name}:${style.getPropertyValue(name)}`)
    .join(';');

  return { contextStyle, paddingSize, borderSize, boxSizing };
}

calcTextareaHeight

calcTextareaHeight是最终暴露出去的函数,用来计算文本域的高度。

export default function calcTextareaHeight(
  targetNode,  // 要计算的结点
  minRows = null,  // 最小的行数
  maxRows = null  // 最大的行数
) {
  if (!hiddenTextarea) {  // 来创建一个隐藏的文本域,所有的计算都是在这上面进行的
    hiddenTextarea = document.createElement('textarea');
    document.body.appendChild(hiddenTextarea);
  }

  // 获取结点一些样式值
  let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
  } = calculateNodeStyling(targetNode);

  // 设置相应的样式
  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
  // 设置内容,按优先级一次是 结点的 value, 结点的 placeholder, 以及空字符串
  hiddenTextarea.value = targetNode.value || targetNode.placeholder || '';

  // 获取滚动高度
  let height = hiddenTextarea.scrollHeight;

  if (boxSizing === 'border-box') {
    // 如果是 border-box,说明高度得加上边框
    height = height + borderSize;
  } else if (boxSizing === 'content-box') {
    // 如果是 content-box,说明得减去上下内边距
    height = height - paddingSize;
  }

  // 计算单行高度,先清空内容
  hiddenTextarea.value = '';
  // 再用滚动高度减去上下内边距
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;

  if (minRows !== null) {  // 如果参数传递了 minRows
    let minHeight = singleRowHeight * minRows; // 说明最少应当有这么多行的高度
    if (boxSizing === 'border-box') {  // 如果是 border-box,还得加上上下内边距和上下边框的宽度
      minHeight = minHeight + paddingSize + borderSize;
    }
    height = Math.max(minHeight, height);  // 取二者最大值
  }
  if (maxRows !== null) {  // 如果参数传递了 maxRows
    let maxHeight = singleRowHeight * maxRows;  // 说明最多只能有这么多行的高度
    if (boxSizing === 'border-box') {  // 如果是 border-box,还得加上上下内边距和上下边框的宽度
      maxHeight = maxHeight + paddingSize + borderSize;
    }
    height = Math.min(maxHeight, height);  // 取二者最小值
  }

  // 返回文本域应当设置的高度
  return { height: height + 'px'};
};

input.vue

input组件较为繁琐,我们一点点分析。

生命周期

created

创建的时候会监听inputSelect事件,并调用inputSelect方法。

created() {
  this.$on('inputSelect', this.inputSelect);
},

inputSelect方法会调用refs上的input的原生的select方法,来选中该input

methods: {
  inputSelect() {
    this.$refs.input.select();
  },
}

mounted

挂载的时候,会调用resizeTextarea方法来设置文本域的大小。

mounted() {
  this.resizeTextarea();
}
methods: {
  resizeTextarea() {
    if (this.$isServer) return;  // 如果是服务端渲染,直接返回,不进行下面的逻辑
    var { autosize, type } = this;
    if (!autosize || type !== 'textarea') return;  // 如果 autosize 是 false,或者当前不是文本域,也直接返回
    const minRows = autosize.minRows;  // 最少行数
    const maxRows = autosize.maxRows;  // 最大行数

    this.textareaStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);  // 计算文本域的高度,并赋值
  },
}

最外层

最外层是一个div,上面设置了一些动态的class

<div :class="[
  type === 'textarea' ? 'el-textarea' : 'el-input',
  size ? 'el-input--' + size : '',
  {
    'is-disabled': disabled,
    'el-input-group': $slots.prepend || $slots.append,
    'el-input-group--append': $slots.append,
    'el-input-group--prepend': $slots.prepend
  }
]">
</div>

type

type是一个prop,它默认设置为text,如果设置为textarea,表明当前是一个文本域。

props: {
  type: {
    type: String,
    default: 'text'
  },
}

size

size也是一个prop,用来设置输入框的大小,在textarea下无效。

props: {
  size: String,
}

disabled

disabled也是一个prop,用来设置是否可用。

props: {
  disabled: Boolean,
}

prepend、append

这两个都是在设置输入框组的时候使用的,通过具名slot传入,分别放置于input的首和尾。

input

然后,根据type的不同使用v-if分别渲染input或者textarea,我们先分析input部分。

前置元素

前置元素直接通过具名slot传入。

<div class="el-input-group__prepend" v-if="$slots.prepend">
  <slot name="prepend"></slot>
</div>

input 图标

图标也是通过具名slot传入的,也可以通过prop中的icon传入图标名。

<slot name="icon">
  <i
    class="el-input__icon"
    :class="'el-icon-' + icon"
    v-if="icon"
    @click="handleIconClick">
  </i>
</slot>

上面还绑定了一个handleIconClick的点击事件,它会触发click事件:

methods: {
  handleIconClick(event) {
    this.$emit('click', event);
  },
}

input

然后是最重要的input部分,上面大部分是prop,不进行讲解,其余的我们将一一讲解。

<input
  v-if="type !== 'textarea'"
  class="el-input__inner"
  :type="type"  // 类型
  :name="name"  // 名字
  :placeholder="placeholder"  // 默认值
  :disabled="disabled"  // 是否禁用
  :readonly="readonly"  // 是否只读
  :maxlength="maxlength"  // 输入的最大长度
  :minlength="minlength"  // 输入的最小长度(暂时不支持)
  :autocomplete="autoComplete"  // 自动补全
  :autofocus="autofocus"  // 自动聚焦
  :min="min"  // 允许输入的最小值(数字或者日期)
  :max="max"  // 允许输入的最大值(数字或者日期)
  :form="form"  // 绑定的表单(不是原生的)
  :value="currentValue"  // 输入值
  ref="input"  // 引用
  @input="handleInput"  // 输入事件
  @focus="handleFocus"  // 获得焦点事件
  @blur="handleBlur"  // 失去焦点事件
>

value

value改变的时候会调用setCurrentValue

watch: {
  'value'(val, oldValue) {
    this.setCurrentValue(val);
  }
},

setCurrentValue是用来改变当前值的。

methods: {
  setCurrentValue(value) {
    if (value === this.currentValue) return;  // 如果新旧值一致直接返回

    this.$nextTick(_ => {
      this.resizeTextarea();  // 下一个DOM更新周期时,重新设置文本域大小
    });

    this.currentValue = value;  // 改变当前值
    this.$emit('input', value);  // 触发 input 事件
    this.$emit('change', value);  // 触发 change 事件
    this.dispatch('ElFormItem', 'el.form.change', [value]);  // 向父级的派发 el.form.change 事件
  }
}

handleInput

处理输入事件。

methods: {
  handleInput(event) {
    this.setCurrentValue(event.target.value);  // 改变当前值
  },
}

handleFocus

handleFocus用来处理获得焦点的事件,会直接触发focus事件。

methods: {
  handleFocus(event) {
    this.$emit('focus', event);
  },
}

handleBlur

handleBlur用来处理失去焦点的事件。

methods: {
  handleBlur(event) {
    this.$emit('blur', event);  // 触发 blur 事件
    this.dispatch('ElFormItem', 'el.form.blur', [this.currentValue]);  // 向父组件派发 el.form.blur 事件
  },
}

loading

loading会根据计算属性validating来决定是否渲染。

computed: {
  validating() {
    return this.$parent.validateState === 'validating';
  }
},
<i class="el-input__icon el-icon-loading" v-if="validating"></i>

后置元素

后置元素只能根据具名slot传入。

<div class="el-input-group__append" v-if="$slots.append">
  <slot name="append"></slot>
</div>

Textarea

如果type设置为textarea则会渲染textarea,上面绑定的都和input类似,不再多说,多了一个textareaStyle,是根据calcTextareaHeight计算出来的。

<textarea
  v-else
  class="el-textarea__inner"
  :value="currentValue"
  @input="handleInput"
  ref="textarea"
  :name="name"
  :placeholder="placeholder"
  :disabled="disabled"
  :style="textareaStyle"
  :readonly="readonly"
  :rows="rows"
  :form="form"
  :autofocus="autofocus"
  :maxlength="maxlength"
  :minlength="minlength"
  @focus="handleFocus"
  @blur="handleBlur">
</textarea>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,048评论 18 139
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 4,971评论 0 29
  • 下载安装搭建环境 可以选npm安装,或者简单下载一个开发版的vue.js文件 浏览器打开加载有vue的文档时,控制...
    冥冥2017阅读 5,973评论 0 42
  • 什么是组件 组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用...
    angelwgh阅读 757评论 0 0
  • 此文基于官方文档,里面部分例子有改动,加上了一些自己的理解 什么是组件? 组件(Component)是 Vue.j...
    陆志均阅读 3,743评论 5 14