基于Vue 自定义指令的多选菜单实现 v-item-select

1. 最近刚学了 vue 自定义指令,突发奇想做一个多选菜单(因为这样的需求其实蛮普遍的),代码高复用低耦合,花了一天时间完成,源码如下:

2. 举个栗子 src/views/DirectiveDemo.vue:

<template>
    <van-nav-bar title="自定义指令" left-arrow  @click-left="onClickLeft"/>
    <h1># 自定义指令</h1>
    <my-item-select
        :list="list"
        :initailIndex="initailIndex"
        >
    </my-item-select>
</template>


<script setup>
import { ref, defineProps, getCurrentInstance } from 'vue';
import MyTab from '@/components/MyTab.vue';
import MyTabSelect from '../components/MyTabSelect.vue';
import MyItemSelect from '../components/MyItemSelect.vue';


const onClickLeft = () => history.back();

// const instance = getCurrentInstance();
// console.log(instance.type.__file);

const props = defineProps({
    list: {
        type: Array,
        // eslint-disable-next-line vue/require-valid-default-prop
        default: [
            { id: 1, title: "选项1", content: "内容1", },
            { id: 2, title: "选项2", content: "内容2", },
            { id: 3, title: "选项3", content: "内容3", },
            { id: 4, title: "选项4", content: "内容4", },
            { id: 5, title: "选项5", content: "内容5", },
            { id: 6, title: "选项6", content: "内容6", },

        ],
    },
    initailIndex: {
        type: [Number, String],
        default: 1
    }
})

</script>

3. 菜单组件 src/components/MyItemSelect.vue:

在这里设置选中已经未选中样式:

<template>
    <h1>单选/多选 v-item-select</h1>
    <div>
        <div v-item-select="{
            itemClass: 'item',
            selectClass: 'item-select',
            currentIndex: currentIdx,
            isMultiple: true,
            min: 1,
            max: 5,
            list: list,
            minblock: minblock,
            maxblock: maxblock,
            block: block,
            }">
            <a
                class='item'
                v-for="(item, idx) in list" :key="idx"
                @click="changeItem(idx)"
                >
                {{item.title}}
            </a>
        </div>    
    </div>
</template>


<script setup>
import { Toast } from 'vant';
import { ref, reactive, getCurrentInstance, computed, defineProps } from 'vue';


const instance = getCurrentInstance();
console.log(instance.type.__file, instance);

const props = defineProps({
    list: {
        type: Array,
        // eslint-disable-next-line vue/require-valid-default-prop
        default () {
            return [];
        },
    },
    initailIndex: {
        type: [Number, String],
        default: 0
    }
})

const currentIndex = ref(props.initailIndex);

const currentContent = computed(() => {
   return props.list[currentIndex.value].content;
})


const changeIndex = (index) => {
    currentIndex.value = index;
}


const currentIdx = ref(props.initailIndex);

const selectItems = reactive([props.list[props.initailIndex]])

const changeItem = (idx) => {
    currentIdx.value = idx;
    // console.log(currentIdx.value, selectIndexs);
    console.log(instance.type.__file, currentIdx.value, selectItems);
}

const block = (indexs, items, idx) => {
    console.log("block", instance.type.__file, indexs, items.map((e) => {
        return e.title
    }));
}

const minblock = (val) => {
    Toast(`数量不能小于 ${val}`)
}

const maxblock = (val) => {
    Toast(`数量不能大于 ${val}`)
}

</script>


<style scoped lang="scss">
    
a{
    font-size: 1rem;
    margin: 8px;
    &.active{
        text-decoration: none;
        color: #000;
        // border: 1px solid #000;
        border-bottom: 1.5px solid #000;
    }
}

// .item{
//     &.item-select{
//         color: red;
//         // border-bottom: 1.5px solid #000;
//         text-decoration: underline;
//     }
// }

.item{
    margin: 8px;

    color: red;
}

.item-select{
    border: 1.5px solid #000;
}

</style>

4. 指令源码:

directives/itemSelect.js


export default { 
    mounted (el, bindings, vnode) {
        // console.log(el, bindings, vnode);
        const {itemClass, selectClass, currentIndex, isMultiple, min, max, list, block, minblock, maxblock} = bindings.value;
        
        el.itemClass = itemClass;
        el.selectClass = selectClass;
        el.items = el.getElementsByClassName(itemClass);
        el.items[currentIndex].className = `${itemClass} ${selectClass}`

        el.isMultiple = isMultiple;
        el.block = block;
        el.list = list;

        block([currentIndex], [list[currentIndex]], currentIndex)
        // console.log("el.items", typeof el.items, el.items);        
        if (!el.isMultiple) {
            return
        }

        var items = Array.from(el.items);

        // console.log("arr", typeof items)
        items.forEach((e, i) => {
            // console.log(e, i);
            e.addEventListener("click", (t) => {
                // console.log("addEventListener >>>", e.target.__vnode.key);
                const idx = t.target.__vnode.key;
                // console.log("addEventListener >>>", idx, el.items[idx].className);
                const isSelected = items[idx].className.endsWith(selectClass);

                let indexs = []
                items.forEach((e, j) => {
                    if (e.className.endsWith(selectClass)) {
                        indexs.push(j) 
                    }
                })
                
                if (min !== undefined && min > 0 
                    && max !== undefined && max > 0) {
                    if (isSelected) {
                        if (indexs.length > min) {
                            items[idx].className = `${itemClass}`
                            indexs.remove(idx)
                        } else {
                            minblock ? minblock(min) : alert(`数量不能小于 ${min}`);
                        }
                    } else {
                        if (indexs.length < max) {
                            items[idx].className = `${itemClass} ${selectClass}`
                            indexs.push(idx) 
                        } else {
                            maxblock ? maxblock(max) : alert(`数量不能大于 ${max}`);
                        }
                    } 
                } else {
                    if (isSelected) {
                        items[idx].className = `${itemClass}`
                        indexs.remove(idx)
                    } else {
                        items[idx].className = `${itemClass} ${selectClass}`
                        indexs.push(idx) 
                    }
                }

                let selectItems = indexs.sort().map((k) => list[k])
                block(indexs.sort(), selectItems, idx)
            });
        });
    },
    updated(el, bindings) {
        const { currentIndex } = bindings.value;
        const oldIndex = bindings.oldValue.currentIndex;
        const {itemClass, selectClass, items } = el;
        // console.log(currentIndex, oldIndex, itemClass,  selectClass, oitems);

        if (el.isMultiple) {
            return
        }
        items[oldIndex].className = itemClass;
        items[currentIndex].className = `${itemClass} ${selectClass}`
        el.block([currentIndex], [el.list[currentIndex]], currentIndex)
    },
}

// const itemSelect = (el, binding) => {
//     el.style.border = "1px solid blue";
// }
  
// export default itemSelect

Array.prototype.remove = function(val) {
    const index = this.indexOf(val);
    if (index >= 0) {
        this.splice(index, 1);
    }
    return this;
}

5. 备注

指令支持多选及单选, 组件参数如下:

itemClass, //子元素 class;
selectClass, //选中状态下子元素 class;
currentIndex, //当前选择索引;
isMultiple, //是否支持多选, 仅为真支持多选,不传或者假单选;
min, //最小选择个数(单选无效,可不传);
max, //最大选择个数(单选无效,可不传);
list, // item 集合;
block, //选择回调;
minblock, // 到达最小值时回调(单选无效,可不传);
maxblock // 到达最大值时回调(单选无效,可不传);

github

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

推荐阅读更多精彩内容