用 vue 組件自定義 v-model, 實現一個 Tab 組件。

效果

先讓咱們看一下例子的效果吧!

例子

v-model

咱們知道 v-model 是 vue 裏面的一個指令,它能夠用在 input 標籤上,來作數據的雙向綁定,就像這樣:html

<input v-model="tab">

v-model 事實上是一個語法糖,你也能夠這麼寫:vue

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

能夠看得出來,就是傳進去一個參數 :value,監聽一個事件 @input 而已。
若是有這樣的需求,須要在本身的組件上使用 v-model,就像這樣:less

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

如何來實現呢?
既然已經知道 v-model 是語法糖了,
那麼首先,咱們能夠知道在組件內獲得的參數。ide

<!-- Tab.vue -->
<template>
    <div class="tab">
        <p>能夠試着把這個值打印出來???</p>
        {{value}}
    </div>
</template>


<script>
    export default {
        props: {
            // ↓這個就是咱們能取到的參數
            value: {
                type: String,
                default: ''
            }
        }
    }
</script>

嗯,先把這個 value 先放着,若是要實現例子的那個 Tab,還須要傳進來一組選項(options):ui

<!-- 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 循環出來吧!this

<!-- 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。spa

<!-- 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 的變化就能夠了,最後貼上完整代碼。code

完整代碼

<!-- 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>

最後送上官網的連接→ 傳送門component

相關文章
相關標籤/搜索