Vue组件实战之ivew源码button篇

源码

<template>
    <div :class="wrapClasses">
        
        <template v-if="type !== 'textarea'">
            
            <div :class="[prefixCls + '-group-prepend']" v-if="prepend" v-show="slotReady"><slot name="prepend">slot>div>
            <i class="ivu-icon" :class="['ivu-icon-' + icon, prefixCls + '-icon', prefixCls + '-icon-normal']" v-if="icon" @click="handleIconClick">i>
            <transition name="fade">
                <i class="ivu-icon ivu-icon-load-c ivu-load-loop" :class="[prefixCls + '-icon', prefixCls + '-icon-validate']" v-if="!icon">i>
            transition>
            <input
                :id="elementId"
                :autocomplete="autocomplete"
                :spellcheck="spellcheck"
                ref="input"
                :type="type"
                :class="inputClasses"
                :placeholder="placeholder"
                :disabled="disabled"
                :maxlength="maxlength"
                :readonly="readonly"
                :name="name"
                :value="currentValue"
                :number="number"
                :autofocus="autofocus"
                @keyup.enter="handleEnter"
                @keyup="handleKeyup"
                @keypress="handleKeypress"
                @keydown="handleKeydown"
                @focus="handleFocus"
                @blur="handleBlur"
                @input="handleInput"
                @change="handleChange">
            
            <div :class="[prefixCls + '-group-append']" v-if="append" v-show="slotReady"><slot name="append">slot>div>
        template>
        
        <textarea
            v-else
            :id="elementId"
            :autocomplete="autocomplete"
            :spellcheck="spellcheck"
            ref="textarea"
            :class="textareaClasses"
            :style="textareaStyles"
            :placeholder="placeholder"
            :disabled="disabled"
            :rows="rows"
            :maxlength="maxlength"
            :readonly="readonly"
            :name="name"
            :value="currentValue"
            :autofocus="autofocus"
            @keyup.enter="handleEnter"
            @keyup="handleKeyup"
            @keypress="handleKeypress"
            @keydown="handleKeydown"
            @focus="handleFocus"
            @blur="handleBlur"
            @input="handleInput">
        textarea>
    div>
template>
<script>
    import { oneOf, findComponentUpward } from '../../utils/assist';
    import calcTextareaHeight from '../../utils/calcTextareaHeight';
    import Emitter from '../../mixins/emitter';

    const prefixCls = 'ivu-input';

    export default {
        name: 'Input',
        // 将引入的 Emitter 中的内容混合到本组件中
        mixins: [ Emitter ],
        props: {
            // 输入框类型,必须为指定数组中的内容,默认值为 text
            type: {
                validator (value) {
                    return oneOf(value, ['text', 'textarea', 'password', 'url', 'email', 'date']);
                },
                default: 'text'
            },
            // 绑定的值,类型为字符串或者数据,默认为空串,一般使用 v-model 双向绑定
            value: {
                type: [String, Number],
                default: ''
            },
            // 输入框尺寸,必须为指定值中的一个
            size: {
                validator (value) {
                    return oneOf(value, ['small', 'large', 'default']);
                }
            },
            // 占位文本
            placeholder: {
                type: String,
                default: ''
            },
            // 最大输入长度
            maxlength: {
                type: Number
            },
            // 禁用状态,默认不禁用
            disabled: {
                type: Boolean,
                default: false
            },
            // 输入框尾部图标,用于向 Icon 组件传递值,只在 text 类型下有效
            icon: String,
            // 自适应内容高度,用于控制 textarea 的高度,也可以传入 Ojbect 来确定输入框的高度,只在 textara 下有效
            autosize: {
                type: [Boolean, Object],
                default: false
            },
            // 文本域默认行数,仅在 textarea 下有效,默认为 2 行
            rows: {
                type: Number,
                default: 2
            },
            // 只读状态,默认可写
            readonly: {
                type: Boolean,
                default: false
            },
            // 元素name
            name: {
                type: String
            },
            // 将用户输入转换为 Number 类型,默认不开启
            number: {
                type: Boolean,
                default: false
            },
            // 自动获取焦点,默认不开启
            autofocus: {
                type: Boolean,
                default: false
            },
            // 拼写检查,默认不开启
            spellcheck: {
                type: Boolean,
                default: false
            },
            // 自动补全,传入值必须为 on, off 之一,默认为 off
            autocomplete: {
                validator (value) {
                    return oneOf(value, ['on', 'off']);
                },
                default: 'off'
            },
            // 元素 id 
            elementId: {
                type: String
            }
        },
        data () {
            return {
                currentValue: this.value,
                prefixCls: prefixCls,
                prepend: true,
                append: true,
                slotReady: false,
                textareaStyles: {}
            };
        },
        computed: {
            wrapClasses () {
                return [
                    `${prefixCls}-wrapper`,
                    {
                        // 根据传入的 size 决定包裹标签的 size 类名,与是否渲染此类
                        [`${prefixCls}-wrapper-${this.size}`]: !!this.size,
                        // 根据传入的 type 决定包裹标签使用的 type 类
                        [`${prefixCls}-type`]: this.type,
                        // 根据是否有前后置内容来决定是否渲染 group 类
                        [`${prefixCls}-group`]: this.prepend || this.append,
                        // 如果设置了 size 且有前后置内容,那么设置对应的 group size 类
                        [`${prefixCls}-group-${this.size}`]: (this.prepend || this.append) && !!this.size,
                        // 根据是否有前后置内容来确定样式
                        [`${prefixCls}-group-with-prepend`]: this.prepend,
                        [`${prefixCls}-group-with-append`]: this.append,
                        [`${prefixCls}-hide-icon`]: this.append
                    }
                ];
            },
            inputClasses () {
                return [
                    // 基础的 button 样式
                    `${prefixCls}`,
                    {
                        // 根据设置的 size 来设置对应的 size 样式
                        [`${prefixCls}-${this.size}`]: !!this.size,
                        // 根据设置的禁用状态来设置 disabled 样式
                        [`${prefixCls}-disabled`]: this.disabled
                    }
                ];
            },
            textareaClasses () {
                return [
                    `${prefixCls}`,
                    {
                        [`${prefixCls}-disabled`]: this.disabled
                    }
                ];
            }
        },
        methods: {
            handleEnter (event) {
                this.$emit('on-enter', event);
            },
            handleKeydown (event) {
                this.$emit('on-keydown', event);
            },
            handleKeypress(event) {
                this.$emit('on-keypress', event);
            },
            handleKeyup (event) {
                this.$emit('on-keyup', event);
            },
            handleIconClick (event) {
                this.$emit('on-click', event);
            },
            handleFocus (event) {
                this.$emit('on-focus', event);
            },
            handleBlur (event) {
                this.$emit('on-blur', event);
                // 如果触发 blur 事件的不是 'DatePicker', 'TimePicker', 'Cascader', 'Search'中的一个,那么对于 FormItem 组件触发 on-form-blur 事件
                if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
                    this.dispatch('FormItem', 'on-form-blur', this.currentValue);
                }
            },
            handleInput (event) {
                let value = event.target.value;
                // 如果开启了转化成数字模式,那么转化成数字
                if (this.number) value = Number.isNaN(Number(value)) ? value : Number(value);
                this.$emit('input', value);
                this.setCurrentValue(value);
                this.$emit('on-change', event);
            },
            handleChange (event) {
                this.$emit('on-input-change', event);
            },
            setCurrentValue (value) {
                if (value === this.currentValue) return;
                // 立即执行 resizeTextara()
                this.$nextTick(() => {
                    this.resizeTextarea();
                });
                // 更新内容
                this.currentValue = value;
                // 如果当前组件的父组件不是 'DatePicker', 'TimePicker', 'Cascader', 'Search'中的一个,那么对于 FormItem 组件触发 on-form-change 事件
                if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
                    this.dispatch('FormItem', 'on-form-change', value);
                }
            },
            resizeTextarea () {
                const autosize = this.autosize;
                // 禁止自适应高度或者不是 textarea 那么可以不操作
                if (!autosize || this.type !== 'textarea') {
                    return false;
                }

                const minRows = autosize.minRows;
                const maxRows = autosize.maxRows;
                // 计算应该要有的样式,然后更新
                this.textareaStyles = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
            },
            focus () {
                if (this.type === 'textarea') {
                    this.$refs.textarea.focus();
                } else {
                    this.$refs.input.focus();
                }
            },
            blur () {
                if (this.type === 'textarea') {
                    this.$refs.textarea.blur();
                } else {
                    this.$refs.input.blur();
                }
            }
        },
        watch: {
            // 其实就是把 props 中传入的 value 与本组件内的 currentValue 内容同步
            value (val) {
                this.setCurrentValue(val);
            }
        },
        mounted () {
            // 如果根据 type 类型与是否传入插槽内容来判断插槽内容是否显示出来
            if (this.type !== 'textarea') {
                this.prepend = this.$slots.prepend !== undefined;
                this.append = this.$slots.append !== undefined;
            } else {
                this.prepend = false;
                this.append = false;
            }
            this.slotReady = true;
            this.resizeTextarea();
        }
    };
script>
/**
 * 寻找指定名称的父组件
 * 
 * @param {any} context 
 * @param {any} componentName 
 * @param {any} componentNames 
 * @returns 
 */
function findComponentUpward (context, componentName, componentNames) {
    // 如果第二个参数是字符串,那就说明没有传入第三个参数,修改一下,使得参数统一
    if (typeof componentName === 'string') {
        componentNames = [componentName];
    } else {
        componentNames = componentName;
    }
    // 取传入组件的父组件
    let parent = context.$parent;
    // 取传入组件的父组件名称
    let name = parent.$options.name;
    // 如果父组件存在,但是父组件名称没有或者不在指定的名称列表中,那么循环,递归向上查找
    while (parent && (!name || componentNames.indexOf(name) < 0)) {
        parent = parent.$parent;
        if (parent) name = parent.$options.name;
    }
    // 返回最终找到的父组件,因为 Vue 规定顶层必须要有一个标签用于注册,所以不用担心找不到
    return parent;
}
// emmiter.js

// 广播事件
function broadcast(componentName, eventName, params) {
    // 遍历当前组件的子组件,并执行操作
    this.$children.forEach(child => {
        const name = child.$options.name;
        // 先取子组件的名称

        // 如果子组件名称等于 componentName,那么子组件触发 eventName 事件,传入参数 params
        // 如果子组件名称不等于 componentName, 那么递归调用,查找孙组件
        if (name === componentName) {
            child.$emit.apply(child, [eventName].concat(params));
        } else {
            // todo 如果 params 是空数组,接收到的会是 undefined
            broadcast.apply(child, [componentName, eventName].concat([params]));
        }
    });
}
export default {
    methods: {
        // 触发 componentName 组件的 eventName 事件,参数是 params
        dispatch(componentName, eventName, params) {
            // 找寻当前组件的父组件,如果不存在,那么直接使用根组件,赋值给 parent
            let parent = this.$parent || this.$root;
            // 取 parent 组件的名称
            let name = parent.$options.name;
            // 如果 parent 存在,但是 parent 名称没有或者不为指定值,那么循环,递归向上查找
            while (parent && (!name || name !== componentName)) {
                parent = parent.$parent;

                if (parent) {
                    name = parent.$options.name;
                }
            }
            // 如果 parent 存在,那么 parent 触发 eventName 事件并传入参数 params
            // [eventName].concat(params) 这里这么写的原因是,调用 apply 方法,参数是要作为一个数组,一次性传入的,也可以这么写:
            // parent.$emit.call(parent, eventName, params);
            if (parent) {
                parent.$emit.apply(parent, [eventName].concat(params));
            }
        },
        // 广播事件
        broadcast(componentName, eventName, params) {
            // 调用 broadcast 方法并传入对应的参数
            broadcast.call(this, componentName, eventName, params);
        }
    }
};
// calcTextareaHeight.js

let hiddenTextarea;

// 隐藏时的样式
const HIDDEN_STYLE = `
    height:0 !important;
    min-height:0 !important;
    max-height:none !important;
    visibility:hidden !important;
    overflow:hidden !important;
    position:absolute !important;
    z-index:-1000 !important;
    top:0 !important;
    right:0 !important
`;

// 内容样式
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'
];

// 计算节点样式
function calculateNodeStyling(node) {
    // 使用 window.getComputedStyle() 方法来获取指定节点在页面上渲染出来的样式
    const style = window.getComputedStyle(node);

    // 拿到指定节点 box-sizing 的值,可以使用 getPropertyValue() 方法来获取,也可以直接使用 style.boxSizing
    const boxSizing = style.getPropertyValue('box-sizing');

    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};
}

export default function calcTextareaHeight(targetNode, minRows = null, maxRows = null) {
    // 如果 hiddenTextarea 不存在
    if (!hiddenTextarea) {
        // 创建一个 textarea 对象赋值给 hiddenTextarea ,并插入到 body 中
        hiddenTextarea = document.createElement('textarea');
        document.body.appendChild(hiddenTextarea);
    }

    // 计算指定节点的样式,并通过解构赋值赋给 paddingSize, borderSize, boxSizing, contextStyle 中
    let {
        paddingSize,
        borderSize,
        boxSizing,
        contextStyle
    } = calculateNodeStyling(targetNode);

    // 给 hiddenTextarea 设置 style ,主要是设置盒模型类型与隐藏样式
    hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
    // 通过短路方式给 hiddenTextarea 设置值
    hiddenTextarea.value = targetNode.value || targetNode.placeholder || '';

    let height = hiddenTextarea.scrollHeight;
    let minHeight = -Infinity;
    let maxHeight = Infinity;

    // 根据盒模型的类型与内容的高度给设置内容高度
    if (boxSizing === 'border-box') {
        height = height + borderSize;
    } else if (boxSizing === 'content-box') {
        height = height - paddingSize;
    }

    // 清空内容
    hiddenTextarea.value = '';
    // 计算出单行高度
    let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;

    // 如果设置了最小/大行数,那么根据行数计算最小高度
    if (minRows !== null) {
        minHeight = singleRowHeight * minRows;
        if (boxSizing === 'border-box') {
            minHeight = minHeight + paddingSize + borderSize;
        }
        height = Math.max(minHeight, height);
    }
    if (maxRows !== null) {
        maxHeight = singleRowHeight * maxRows;
        if (boxSizing === 'border-box') {
            maxHeight = maxHeight + paddingSize + borderSize;
        }
        height = Math.min(maxHeight, height);
    }

    // 返回计算的高度与最小高度,最大高度
    return {
        height: `${height}px`,
        minHeight: `${minHeight}px`,
        maxHeight: `${maxHeight}px`
    };
}

用法

// value
<Input v-model="value">Input>

// size
<Input v-model="value" size="large">Input>

// placehoder
<Input v-model="value" placeholder="large size">Input>

// icon
<Input v-model="value" icon="ios-clock-outline">Input>

// type
<Input v-model="value" type="textarea">Input>
<Input v-model="value" type="password">Input>
<Input v-model="value" type="url">Input>
<Input v-model="value" type="email">Input>
<Input v-model="value" type="date">Input>

// textarea autosize
<Input v-model="value" type="textarea" :autosize="true">Input>
<Input v-model="value8" type="textarea" :autosize="{minRows: 2,maxRows: 5}">Input>

// disabled
<Input v-model="value" disabled>Input>

// prepend append slot
<Input v-model="value11">
    <span slot="prepend">http://span>
    <span slot="append">.comspan>
Input>

想法

  • Vue 过渡动画的使用

    将需要过渡动画的内容使用 transition 标签包起来, transition 标签仅需要 name 属性用于指定操作动画的 CSS 类名,一共有六组,执行顺序如下,选填

    graph LR
    nameAttributeValue-enter-->nameAttributeValue-enter-active
    nameAttributeValue-enter-active-->nameAttributeValue-enter-to
    nameAttributeValue-enter-to-->nameAttributeValue-leave
    nameAttributeValue-leave-->nameAttributeValue-leave-active
    nameAttributeValue-leave-active-->nameAttributeValue-leave-to
  • Vue $nextTick方法的使用
    由于VUE的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。主要是用于优化视图更新时间,减少重复渲染.

你可能感兴趣的:(vuejs学习笔记,ivew,vue,vuejs,源码,组件)