美文网首页Element非官方分析
Element分析(组件篇)——Input

Element分析(组件篇)——Input

作者: liril | 来源:发表于2017-01-01 22:57 被阅读14277次

    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>
    

    相关文章

      网友评论

      • zackxizi:请问源码中没有字体图标:on-icon-click的属性,然后在<el-input
        placeholder="请选择日期"
        icon="search"
        v-model="input2"
        :on-icon-click="handleIconClick">
        </el-input>中却含带了:on-icon-click

      本文标题:Element分析(组件篇)——Input

      本文链接:https://www.haomeiwen.com/subject/vvbivttx.html