This package only supports Vue ^3.2.x
Vue-Selectable-Items is a data-oriented item rendering/selecting engine, it has zero styling and minimal functioning and all of its parts are configurable.
It is as type safe as possible.
Install it with your favorite package manager
npm install vue-selectable-items
pnpm add vue-selectable-items
yarn add vue-selectable-items
<script setup>
import { ref } from 'vue';
import { SelectableItems, item, customItem, itemGroup };
const items = [
item({
key: 'item-key-1',
meta: { title: 'New York City' },
}),
customItem({
key: 'label-1',
name: 'label',
meta: {
onClick: () => doSomething(),
someProp: 123
},
}),
itemGroup({
key: 'group-x',
items: [
item({
key: 'randomKey',
meta: { title: 'Hello' }
})
]
})
]
</script>
<template>
<SelectableItems :items="items">
<template #render="metaData"> {{ metaData.title }} </template>
<template #label="metaData">
<some-component v-bind="metaData" />
</template>
</SelectableItems>
</template>
Let's explain everything step by step.
-
Main item renderer component, you can render items however you want with slots.
-
Props:
import { type Context } from 'vue-selectable-items'; interface SelectableItemProps { // Items for component to render/handle. items: (Item | ItemGroup | CustomItem)[]; // If `true` given, SelectableItems will have no wrapper element, // just stack of items (Fragment). noWrapperElement?: booolean; // If you want to add setup specific things: // onMounted, Composables, Keyboard Handling (see examples) and more setup specific stuff. // NOTE: This function will only run once on setup phase. setup?: (context: Context) => void; // Default options for all selectable items. itemDefaults?: ItemDefaults; }
-
Emits:
interface Emits { // Triggered when an item is selected. select(meta: any, item: Item<any>, el: HTMLElement): void; // Triggered when an item is focused. // This is for virtual focus of SelectableItems, not DOM focus itemFocus(meta: any, item: Item<any>, el: HTMLElement): void; // For DOM focus, this even can be used. itemDOMFocus(e: FocusEvent, meta: any, item: Item<any>): void; // Triggered when an item is unfocused/blurred. itemUnfocus(meta: any, item: Item<any>, el: HTMLElement): void; // Triggered when an item is focused by mouse // `focus` event is also called but before `hover` event. // programmatic focus, won't trigger this event. itemHover(meta: any, item: Item<any>, el: HTMLElement): void; }
-
Type
function item<Meta = unknown>(options: ItemOptions<Meta>): Item<Meta>;
-
Usage
This is main function that creates a selectable item and will be rendered as
render
slot on component.You can store any
meta
data in it and item will be passed as scoped slot<template #render="item" />
for render use, you can access meta data with{ meta }
oritem.meta
.Selectable items are always rendered by
render
.<script setup> import { item } from 'vue-selectable-items' import { ref } from 'vue' const items = ref([ item({ key: 'my-item-unique-key', // Metadata, this will be accessable in slot data. You can give any value. meta: { title: 'Hello' } // You can specifiy item specific event handler, this will be called when this item selected. // NOTE: `select` emit will be called always. onSelect: (meta, thisItem, element) => console.log('Selected', metaData.title); }) ]); </script> <template> <SelectableItems :items="items"> <!-- This slot will be called for each selectable item, so meta can be different for some items, it's up to you. --> <template #render="{ meta }"> {{ meta.title }} </template> </SelectableItems> <!-- This is what above code renders to DOM --> <div class="vue-selectable-items"> <!-- Items are always wrapped by a div. --> <div class="vue-selectable-item">Hello</div> </div> </template>
-
Options
item<Meta, Wrapper, Element>(options: ItemOptions<Meta, Wrapper, Element>): Item<Meta, Wrapper, Element>; interface ItemOptions< Meta, WrapperComponentOrTag, ElementTag, // WrapperProps = GetWrapperComponentOrTagProps<WrapperComponent> ElementAttrs = GetAttrs<ElementTag> > { /** * Required unique key for this item. */ key: string; /** * Meta data this item will store. */ meta: Meta; /** * Determines if item disabled. * Disabled items have `vue-selectable-items-item-disabled` class. */ disabled?: boolean; /** * You can wrap this item with a Component or an element. * If you pass component, item will be rendered on `default` slot of given component. * If you pass string, that string will be used as tag. Pass `div, span` etc. */ wrapperComponentOrTag?: WrapperComponentOrTag; /** * Given attrs/props will be passed to `wrapperComponentOrTag` * For example if you passed `Teleport`to `wrapperComponentOrTag` you can * pass `wrapperProps: { to: 'body' }`, and `Teleport` will use these props. */ wrapperProps?: WrapperProps; /** * Tag of selectable element, by default it is `div.vue-selectable-item` * You can set a custom tag like `span`, `button` etc. */ elementTag?: ElementTag; /** * Attrs that will be passed to selectable item. * For example if you set `elementTag` to a button, you can set it's type by passing * `elementAttrs: { type: 'submit' }` */ elementAttrs?: ElementAttrs, // Item specific selection event. onSelect?: (meta: Meta) => void; }
-
Type
function customItem<Meta>(options: CustomItemOptions<Meta>): CustomItem<Meta>;
-
Usage
Custom item provides a way to add
non-selectable
item like elements respected by queue.CustomItem doesn't have a wrapper element by default, what is given is rendered.
You have to specify the slot name of customItem will be rendered to and meta will be passed as slot data instead of whole item data.
<script setup> import { customItem } from 'vue-selectable-items' import { ref } from 'vue' const items = ref([ customItem({ key: 'sp3ci4lK€Y' meta: 'Cities' name: 'label' }) ]); const realWorldExample = ref([ customItem({ key: 'label:toyota', meta: 'Toyota', name: 'label' }), item({ key: 'corolla', meta: { name: 'Corolla' } }), item({ key: 'supra', meta: { name: 'Supra' } }), customItem({ key: 'label:volkswagen', meta: 'Volkswagen', name: 'label' }), item({ key: 'golf', meta: { name: 'Golf' } }), item({ key: 'id5' meta: { name: 'ID.5' } }), ]); </script> <template> <SelectableItems :items="items"> <!-- This slot will be called for each custom item that has `label` as name. --> <template #label="title"> <h5>{{ title }}</h5> </template> </SelectableItems> <!-- This is what above code renders to DOM --> <div class="vue-selectable-items"> <h5>Cities</h5> </div> </template>
-
Options
function customItem<Meta>(options: CustomItemOptions<Meta>): CustomItem<Meta>; interface CustomItemOptions<Meta> { key: string; meta: Meta; /** * This custom item will be rendered in given name as slotName. */ name: string; }
-
Type
function itemGroup<WrapperComponent>( options?: ItemGroupOptions<WrapperComponent>, ): ItemGroup<WrapperComponent>;
-
Usage
itemGroup is a way to group items under a structure. By default
itemGroup
has no effect on render but ifwrapperComponentOrTag
field is set itemGroup's all items will be rendered in given wrapper component.Items of
itemGroup
will be rendered by given order.<script setup> import { itemGroup, customItem, item } from 'vue-selectable-items' import GroupWrapper from './GroupWrapper.vue' import { ref } from 'vue' const items = ref([ customItem({ key: 'cities:label', meta: 'Cities', name: 'label' }), item({ key: 'greetingsItem', meta: 'Hello' }), itemGroup({ key: 'my_item_group_and_needs_key', items: [ item({ key: 'question:how', meta: 'How are you?' }), customItem({ key: 'label:countries', meta: 'Countries', name: 'label' }), item({ key: 'mySpecialItem' meta: { text: 'New York', type: 'something' }, }) ], wrapperComponentOrTag: GroupWrapper, wrapperProps: { title: 'Suggested Selections' } }) ]); </script> <template> <SelectableItems :items="items"> <!-- This slot will be called for each custom item that has `label` as name. --> <template #label="title"> <h5>{{ title }}</h5> </template> <template #render="{ meta }"> <template v-if="typeof meta === 'string'"> {{ meta }} </template> <template v-else> <span> {{ meta.text }} </span> <span> {{ meta.type }} </span> </template> </template> </SelectableItems> <!-- This is what the code above renders to DOM --> <div class="vue-selectable-items"> <h5>Cities</h5> <div class="vue-selectable-items-item">Hello</div> <!-- Think it like rendered component --> <GroupWrapper title="Suggested Selections"> <div class="vue-selectable-items-item">How are you?</div> <h5>Countries</h5> <div class="vue-selectable-items-item"> <span> New York</span> <span> something </span> </div> </GroupWrapper> </div> </template>
You can nest itemGroups infinitely.
const items = [ itemGroup({ key: 'parent', item: [ itemGroup({ key: 'child', item: [...more] }) ] }) ];
-
Options
function itemGroup<WrapperComponent>(options?: ItemGroupOptions<WrapperComponent>): ItemGroup<WrapperComponent>; interface ItemGroupOptions<WrapperComponent, Props = GetComponentOrElementProps<WrapperComponent>> { key: string; items: ItemRenderList; /** * You can wrap this group with a Component or an element. * If you pass component, items will be rendered on `default` slot of given component. * If you pass string, that string will be used as tag. Pass `div, span` etc. */ wrapperComponentOrTag?: WrapperComponent; /** * Given attrs/props will be passed to `wrapperComponentOrTag` * For example if you passed `Teleport`to `wrapperComponentOrTag` you can * pass `wrapperProps: { to: 'body' }`, and `Teleport` will use these props. */ wrapperProps?: Props; }
If you want to give default options to all selectable items, you can use itemDefaults: ItemDefaults
prop.
It is recommended to use createItemDefaults(options?: Partial<ItemDefaults>)
function to create options because if wrapperComponentOrTag
is component it'll be wrapped by markRaw
not to slow performance.
<script setup>
import { createItemDefaults, SelectableItems, items } from 'vue-selectable-items';
import MyWrapper from './MyWrapper.vue';
const items = [
item({
key: 'myitem',
meta: 'Item 1',
elementAttrs: {
onMouseMove() {
console.log('Other thing');
}
}
})
]
const itemDefaults = createItemDefaults({
elementTag: 'button',
// All attrs and props will be merged with item specific options.
elementAttrs: {
tabindex: '0',
outline: 'none',
// This will not override local onMouseMove attr on item, they will be merged.
onMouseMove() {
console.log('Something');
}
},
wrapperComponentOrTag: MyWrapper,
wrapperProps: {
someValue: true
}
})
// You can also use a function to create defaults dynamically.
const itemDefaults2 = createItemDefaults<Meta>((item) => ({
elementAttrs: {
tabindex: item.disabled ? -1 : 0,
role: item.meta?.children ? 'tooltip' : null
},
elementTag: item.meta?.header ? 'h1' : 'div'
}))
</script>
<template>
<SelectableItems :items="items" :itemDefaults="itemDefaults"/>
</template>
Context is an object of functions, you can access it via setup
prop and template ref
.
Both setup
and template ref
has their own advantages.
-
Setup Function
In setup phase, you can create watchers to watch changes, but you lose freedom but you can assign
ctx
to a local variable to access in your own component instance.<script lang="ts" setup> import { type Context, SelectableItems } from 'vue-selectable-items'; import { watch } from 'vue'; let context: Context; function handleSetup(ctx: Context) { // You can carry context to your component scope // But don't forget, only `handleSetup` function will run // in SelectableItems setup phase // Any onMounted or some composable will be dependent on SelectableItems instance. // If you remove `SelectableItems` from render all watchers created inside handleSetup // will be killed. context = ctx; ctx.onHover((meta, item, element) => { element.focus(); }) // You can create computed and watchers here, but don't forget that it is binded to SelectableItems instance. const focusedItem = computed(() => ctx.getFocusedItem()) watch(() => ctx.getFocusedItem(), (newItem, oldItem) => { ... }) } </script> <template> <SelectableItems :items="items" :setup="handleSetup" /> </template>
-
Template Ref
With template ref you can assign context directly to local variable, but you cannot create reactive effects and it will be only available after mount.
You can still control behavior of SelectableItems.
<script lang="ts" setup> import { type Context, SelectableItems } from 'vue-selectable-items'; import { ref, onMounted } from 'vue'; const itemsContext = ref<Context>(); onMounted(() => { itemsContext.value?.scrollToFocusedItemElement(); itemsContext.value?.clearFocus(); }) </script> <template> <SelectableItems :items="items" ref="itemsContext" /> </template>
Here is type of context:
export type Context = {
focusNext(): void;
focusPrevious(): void;
clearFocus(): void;
setFocusByKey(key?: string | null | undefined): void;
setFocusByIndex(index: number): void;
onSelect(fn: SelectHook): void;
onFocus(fn: FocusHook): void;
onUnfocus(fn: UnfocusHook): void;
onDOMFocus(fn: DOMFocusHook): void;
getItemMetaByKey<Meta = any>(key: string): Meta | undefined;
getSelectableItemCount(): number;
getSelectableItems<Meta = unknown>(): Item<Meta>[];
selectFocusedItem(): void;
getFocusedItem<Meta = unknown>(): Item<Meta> | undefined;
getItemElementByKey(key: string): HTMLElement | undefined;
getItemElementByIndex(index: number): HTMLElement | undefined;
scrollToFocusedItemElement(options?: ScrollIntoViewOptions): void;
getFocusedItemElement(): HTMLElement | undefined;
};
-
Never pass single item reference more than once, each item should be unique, if you do, change the key! Duplicate keys will cause errors.
const myItem = item({ key: 'my_itemov', meta: 'My Item', }); const items = [ myItem, myItem, ]; console.log(items); /* { key: 'my_itemov', }, { key: 'my_itemov' // DUPLICATE FOUND } */
-
To get the best type support for
item()
anditemGroup()
while definingwrapperComponentOrTag
andelementTag
asstring
useas const
.Following images will show the difference.