用 vue 组件自定义 v-model, 实现一个 Tab 组件。

viewweiwu

效果

先让我们看一下例子的效果吧!

例子

v-model

我们知道 v-model 是 vue 里面的一个指令,它可以用在 input 标签上,来做数据的双向绑定,就像这样:

<input v-model="tab">

v-model 事实上是一个语法糖,你也可以这么写:

<input :value="tab" @input="tab = $event.target.value">

可以看得出来,就是传进去一个参数 :value,监听一个事件 @input 而已。
如果有这样的需求,需要在自己的组件上使用 v-model,就像这样:

<Tab v-model="tab"></Tab>

如何来实现呢?
既然已经知道 v-model 是语法糖了,
那么首先,我们可以知道在组件内得到的参数。

<!-- Tab.vue -->
<template>
    <div class="tab">
        <p>可以试着把这个值打印出来???</p>
        {{value}}
    </div>
</template>


<script>
    export default {
        props: {
            // ↓这个就是我们能取到的参数
            value: {
                type: String,
                default: ''
            }
        }
    }
</script>

嗯,先把这个 value 先放着,如果要实现例子的那个 Tab,还需要传进来一组选项(options):

<!-- example.vue -->
<template>
    <div>
        <!-- 这里多了一个参数  ↓ -->
        <Tab v-model="tab" :options="options"></Tab>
        <p class="info">{{tab}}</p>
    </div>
</template>

<script>
    import Tab from '~/Tab';

    export default {
        components: {
            Tab
        },
        data() {
            return {
                tab: 'bj',
                options: [{
                    value: 'bj',
                    text: '北京'
                }, {
                    value: 'sh',
                    text: '上海',
                    disabled: true
                }, {
                    value: 'gz',
                    text: '广州'
                }, {
                    value: 'sz',
                    text: '深圳'
                }]
            }
        }
    }
</script>

那我们就把传进来的 options 循环出来吧!

<!-- Tab.vue -->
<template>
    <div class="tab">
        <div 
            class="item"
            v-for="(item, i) in options"
            :key="i">
            {{item.text}}
        </div>
    </div>
</template>

<script>
    export default {
        props: {
            value: {
                type: String
            },
            options: {
                type: Array,
                default: []
            }
        }
    }
</script>

传进来的 options 缺少些参数,我们每个选项需要 active 来标记是否是选中状态,需要 disabled 来标记是否是禁选状态,所以拷贝一个 currOptions 来补全不足参数。
另外直接改变 value 这个 props 是没有效果滴,所以拷贝一个 value 的副本,叫 currValue。

<!-- Tab.vue -->
<script>
    export default {
        props: {
            value: {
                type: String
            },
            options: {
                type: Array,
                default: []
            }
        },
        data() {
            return {
                // 拷贝一个 value
                currValue: this.value,
                currOptions: []
            }
        },
        mounted() {
            this.initOptions();
        },
        methods: {
            initOptions() {
                // 拷贝一个 options
                this.currOptions = this.options.map(item => {
                    return {
                        ...item,
                        active: item.value === this.currValue,
                        disabled: !!item.disabled
                    }
                });
            }
        }
    }
</script>

?接下来再在选项上绑定击事件就 OK 了。
既然知道父组件会接受 input 事件,那我们就只需要 this.$emit('input', this.currValue); 就好了。

<!-- Tab.vue -->
<template>
    <div class="tab">
        <div 
            class="item"
            v-for="(item, i) in options"
            :key="i"
            @click="onTabSelect(item)">
            <!-- ↑ 这里绑定了一个事件! -->
            {{item.text}}
        </div>
    </div>
</template>

<script>
    export default {
        props: {
            value: {
                type: String
            },
            options: {
                type: Array,
                default: []
            }
        },
        data() {
            return {
                currValue: this.value,
                currOptions: []
            }
        },
        mounted() {
            this.initOptions();
        },
        methods: {
            initOptions() {
                this.currOptions = this.options.map(item => {
                    return {
                        ...item,
                        active: item.value === this.currValue,
                        disabled: !!item.disabled
                    }
                });
            },
            // 添加选中事件
            onTabSelect(item) {
                if (item.disabled) return;
                this.currOptions.forEach(obj => obj.active = false);
                item.active = true;
                this.currValue = item.value;
                // 发布 input 事件,↓ 父组件如果有 v-model 就会监听到的。
                this.$emit('input', this.currValue);
            }
        }
    }
</script>

剩下的补上点样式还有 watch 下 value 和 options 的变化就可以了,最后贴上完整代码。

完整代码

<!-- example.vue -->
<template>
    <div>
        <Tab v-model="tab" :options="options"></Tab>
        <p class="info">{{tab}}</p>
    </div>
</template>

<script>
    import Tab from '~/Tab';

    export default {
        components: {
            Tab
        },
        data() {
            return {
                tab: 'bj',
                options: [{
                    value: 'bj',
                    text: '北京'
                }, {
                    value: 'sh',
                    text: '上海',
                    disabled: true
                }, {
                    value: 'gz',
                    text: '广州'
                }, {
                    value: 'sz',
                    text: '深圳'
                }]
            }
        }
    }
</script>

<style lang="less" scoped>
    .info {
        margin-left: 50px;
        font-size: 30px;
    }
</style>
<!-- Tab.vue -->
<template>
    <div class="tab">
        <div 
            class="item"
            v-for="(item, i) in currOptions"
            :class="item | tabItemClass"
            :key="i"
            @click="onTabSelect(item)">
            {{item.text}}
        </div>
    </div>
</template>

<script>
    export default {
        props: {
            value: {
                type: String
            },
            options: {
                type: Array,
                default: []
            }
        },
        data() {
            return {
                currValue: this.value,
                currOptions: []
            }
        },
        mounted() {
            this.initOptions();
        },
        methods: {
            initOptions() {
                this.currOptions = this.options.map(item => {
                    return {
                        ...item,
                        active: item.value === this.currValue,
                        disabled: !!item.disabled
                    }
                });
            },
            onTabSelect(item) {
                if (item.disabled) return;
                this.currOptions.forEach(obj => obj.active = false);
                item.active = true;
                this.currValue = item.value;
                this.$emit('input', this.currValue);
            }
        },
        filters: {
            tabItemClass(item) {
                let classList = [];
                if (item.active) classList.push('active');
                if (item.disabled) classList.push('disabled');
                return classList.join(' ');
            }
        },
        watch: {
            options(value) {
                this.initOptions();
            },
            value(value) {
                this.currValue = value;
            }
        }
    }
</script>

<style lang="less" scoped>
    .tab {
        @borderColor: #ddd;
        @radius: 5px;

        width: 100%;
        margin: 50px;
        overflow: hidden;
        position: relative;
        .item {
            padding: 10px 50px;
            border-top: 1px solid @borderColor;
            border-left: 1px solid @borderColor;
            border-bottom: 1px solid @borderColor;
            font-size: 30px;
            background-color: #fff;
            float: left;
            user-select: none;
            cursor: pointer;
            transition: 300ms;
            &:first-child {
                border-top-left-radius: @radius;
                border-bottom-left-radius: @radius;
            }
            &:last-child {
                border-right: 1px solid @borderColor;
                border-top-right-radius: @radius;
                border-bottom-right-radius: @radius;
            }
            &.active {
                color: #fff;
                background-color: red;
            }
            &:hover {
                color: #fff;
                background-color: #f06;
            }
            &.disabled {
                color: #fff;
                background-color: pink;
                cursor: no-drop;
            }
        }
    }
</style>

最后送上官网的链接→ 传送门

阅读 13k

前端开发日志
小白前端的开发日志。

咸鱼也是有梦想的!

630 声望
102 粉丝
0 条评论

咸鱼也是有梦想的!

630 声望
102 粉丝
文章目录
宣传栏