Skip to content

Commit d6ef91e

Browse files
committed
grouping support in VfSmartSelect
1 parent 4cc0bfe commit d6ef91e

File tree

4 files changed

+91
-33
lines changed

4 files changed

+91
-33
lines changed
Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
<template>
22
<div id="demo-vf-smart-select">
3-
<VfSmartSelect v-model="selectedOption" :options="options" label-field="label" />
3+
<div>
4+
<VfSmartSelect v-model="selectedOption" :options="options" label-field="label" />
45

5-
Selected value: {{ selectedOption?.label ?? '-' }}
6+
Selected value: {{ selectedOption?.label ?? '-' }}
7+
</div>
8+
9+
<div>
10+
<VfSmartSelect v-model="selectedOption" :options="options" label-field="label" group-field="group" />
11+
12+
Selected value: {{ selectedOption?.label ?? '-' }}
13+
</div>
614
</div>
715
</template>
816

@@ -12,19 +20,27 @@ import { ref } from 'vue';
1220
import VfSmartSelect from '@/components/vf-smart-select.vue';
1321
1422
const options = [
15-
{ value: '1', label: 'Option 1' },
16-
{ value: '2', label: 'Option 2' },
17-
{ value: '3', label: 'Option 3' },
18-
{ value: '4', label: 'Option 4' },
19-
{ value: '5', label: 'Option 5' },
20-
{ value: '6', label: 'Option 6' },
21-
{ value: '7', label: 'Option 7' },
22-
{ value: '8', label: 'Option 8' },
23-
{ value: '9', label: 'Option 9' },
24-
{ value: '10', label: 'Option 10' }
23+
{ value: '1', label: 'Option 1', group: 'Set 1' },
24+
{ value: '2', label: 'Option 2', group: 'Set 1' },
25+
{ value: '3', label: 'Option 3', group: 'Set 1' },
26+
{ value: '4', label: 'Option 4', group: 'Set 1' },
27+
{ value: '5', label: 'Option 5', group: 'Set 1' },
28+
{ value: '6', label: 'Option 6', group: 'Set 2' },
29+
{ value: '7', label: 'Option 7', group: 'Set 2' },
30+
{ value: '8', label: 'Option 8', group: 'Set 2' },
31+
{ value: '9', label: 'Option 9', group: 'Set 2' },
32+
{ value: '10', label: 'Option 10', group: 'Set 2' }
2533
];
2634
2735
const selectedOption = ref<(typeof options)[number] | null>(null);
2836
</script>
2937

30-
<style lang="scss" scoped></style>
38+
<style lang="scss" scoped>
39+
#demo-vf-smart-select {
40+
max-width: 450px;
41+
42+
> div {
43+
margin-top: 12px;
44+
}
45+
}
46+
</style>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@signal24/vue-foundation",
33
"type": "module",
4-
"version": "4.24.2",
4+
"version": "4.25.0",
55
"description": "Common components, directives, and helpers for Vue 3 apps",
66
"module": "./dist/vue-foundation.es.js",
77
"exports": {

src/components/vf-smart-select.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface VfSmartSelectOptionDescriptor<T> {
22
key: string | symbol;
3+
group?: string;
34
title: string;
45
subtitle?: string | null;
56
searchContent?: string;

src/components/vf-smart-select.vue

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,42 @@
1515
@focus="handleInputFocused"
1616
@blur="handleInputBlurred"
1717
/>
18-
<div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options">
18+
<div v-if="shouldDisplayOptions" ref="optionsContainer" class="vf-smart-select-options" :class="{ grouped: isGrouped }">
1919
<div v-if="!isLoaded" class="no-results">Loading...</div>
2020
<template v-else>
21-
<div
22-
v-for="option in effectiveOptions"
23-
:key="String(option.key)"
24-
class="option"
25-
:class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
26-
@mousemove="handleOptionHover(option)"
27-
@mousedown="selectOption(option)"
28-
>
29-
<slot name="option" :option="option">
30-
<div class="title" v-html="option.title" />
31-
<div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
32-
</slot>
33-
</div>
34-
<div v-if="!effectiveOptions.length && searchText" class="no-results">
35-
<slot name="no-results">
36-
{{ effectiveNoResultsText }}
37-
</slot>
21+
<div v-for="group in groupedOptions" :key="group.groupTitle" class="group">
22+
<div v-if="group.groupTitle" class="group-title">
23+
<slot name="group" :group="group.groupTitle">
24+
{{ group.groupTitle }}
25+
</slot>
26+
</div>
27+
28+
<div
29+
v-for="option in group.options"
30+
:key="option.key"
31+
class="option"
32+
:class="[highlightedOptionKey === option.key && 'highlighted', option.ref && classForOption?.(option.ref)]"
33+
@mousemove="handleOptionHover(option)"
34+
@mousedown="selectOption(option)"
35+
>
36+
<slot name="option" :option="option">
37+
<div class="title" v-html="option.title" />
38+
<div v-if="option.subtitle" class="subtitle" v-html="option.subtitle" />
39+
</slot>
40+
</div>
41+
<div v-if="!effectiveOptions.length && searchText" class="no-results">
42+
<slot name="no-results">
43+
{{ effectiveNoResultsText }}
44+
</slot>
45+
</div>
3846
</div>
3947
</template>
4048
</div>
4149
</div>
4250
</template>
4351

4452
<script lang="ts" setup generic="T, V = T">
45-
import { debounce, isEqual } from 'lodash';
53+
import { debounce, groupBy, isEqual, uniq } from 'lodash';
4654
import { computed, onMounted, type Ref, ref, watch } from 'vue';
4755
4856
import { escapeHtml } from '../helpers/string';
@@ -69,6 +77,8 @@ const props = defineProps<{
6977
valueField?: keyof T;
7078
valueExtractor?: (option: T) => V;
7179
labelField?: keyof T;
80+
groupField?: keyof T;
81+
groupFormatter?: (option: T) => string;
7282
formatter?: (option: T) => string;
7383
subtitleFormatter?: (option: T) => string;
7484
classForOption?: (option: T) => string;
@@ -129,16 +139,23 @@ const effectiveKeyExtractor = computed(() => {
129139
if (effectiveValueExtractor.value) return (option: T) => String(effectiveValueExtractor.value!(option));
130140
return null;
131141
});
142+
const effectiveGroupFormatter = computed(() => {
143+
if (props.groupFormatter) return props.groupFormatter;
144+
if (props.groupField) return (option: T) => String(option[props.groupField!]);
145+
return null;
146+
});
132147
const effectiveFormatter = computed(() => {
133148
if (props.formatter) return props.formatter;
134149
if (props.labelField) return (option: T) => String(option[props.labelField!]);
135150
return (option: T) => String(option);
136151
});
137152
138153
const allOptions = computed(() => [...effectivePrependOptions.value, ...loadedOptions.value, ...effectiveAppendOptions.value]);
154+
const isGrouped = computed(() => !!(props.groupField || props.groupFormatter));
139155
140156
const optionsDescriptors = computed(() => {
141157
return allOptions.value.map((option, index) => {
158+
const group = effectiveGroupFormatter.value?.(option);
142159
const title = effectiveFormatter.value(option);
143160
const subtitle = props.subtitleFormatter?.(option);
144161
const strippedTitle = title ? title.trim().toLowerCase() : '';
@@ -160,6 +177,7 @@ const optionsDescriptors = computed(() => {
160177
161178
return {
162179
key: effectiveKeyExtractor.value?.(option) ?? String(index),
180+
group,
163181
title,
164182
subtitle,
165183
searchContent: searchContent.join(''),
@@ -208,6 +226,24 @@ const effectiveOptions = computed(() => {
208226
return options;
209227
});
210228
229+
const groupedOptions = computed(() => {
230+
if (!effectiveOptions.value[0]?.group) {
231+
return [
232+
{
233+
groupTitle: '',
234+
options: effectiveOptions.value
235+
}
236+
];
237+
}
238+
239+
const groupTitles = uniq(effectiveOptions.value.map(option => option.group ?? ''));
240+
const groupedOptions = groupBy(effectiveOptions.value, option => option.group);
241+
return groupTitles.map(groupTitle => ({
242+
groupTitle,
243+
options: groupedOptions[groupTitle!]
244+
}));
245+
});
246+
211247
// watch props
212248
watch(() => props.modelValue, handleValueChanged);
213249
watch(
@@ -593,6 +629,11 @@ function focusNextInput() {
593629
overflow: auto;
594630
z-index: 101;
595631
632+
.group-title {
633+
padding: 5px 8px;
634+
color: #999;
635+
}
636+
596637
.option,
597638
.no-results {
598639
padding: 5px 8px;

0 commit comments

Comments
 (0)