Skip to content

Commit ca78c88

Browse files
committed
feat(select): add #tag-content slot
See #323
1 parent 482d85b commit ca78c88

File tree

9 files changed

+431
-2
lines changed

9 files changed

+431
-2
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
title: 'Custom tag content slot'
3+
---
4+
5+
# Custom tag content slot
6+
7+
The following example demonstrates how to use the `VueSelect` component with the `#tag-content` slot when using the `isMulti` prop.
8+
9+
## What is the `#tag-content` slot?
10+
11+
The `#tag-content` slot allows you to customize only the text content inside a multi-select tag, while automatically preserving:
12+
13+
- The tag wrapper structure
14+
- Default styling (background, padding, borders)
15+
- The remove button
16+
- Event handlers for removing tags
17+
18+
This makes it much simpler than the `#tag` slot when you only want to change how the label text is displayed.
19+
20+
## When to use `#tag-content` vs `#tag`
21+
22+
### Use `#tag-content` when:
23+
- You only want to format or transform the label text
24+
- You want to add icons, badges, or emphasis to the text
25+
- You want to keep the default tag styling and remove button
26+
27+
### Use `#tag` when:
28+
- You need complete control over the tag structure
29+
- You want custom styling that can't be achieved with CSS variables
30+
- You need to change the remove button appearance or behavior
31+
32+
::: info
33+
Read more about available [slots here](../slots.md), the `#tag` slot [here](./custom-tag-slot.md), and the `isMulti` prop [here](../props.md#isMulti).
34+
:::
35+
36+
<script setup>
37+
import { ref } from "vue";
38+
import VueSelect from "../../src";
39+
40+
const selected = ref([]);
41+
42+
const skillOptions = [
43+
{ label: "JavaScript", value: "javascript", level: "expert", icon: "🟨" },
44+
{ label: "TypeScript", value: "typescript", level: "expert", icon: "🔷" },
45+
{ label: "Vue.js", value: "vue", level: "expert", icon: "💚" },
46+
{ label: "React", value: "react", level: "intermediate", icon: "⚛️" },
47+
{ label: "Python", value: "python", level: "beginner", icon: "🐍" },
48+
{ label: "Rust", value: "rust", level: "beginner", icon: "🦀" },
49+
];
50+
51+
const levelColors = {
52+
beginner: "#93c5fd",
53+
intermediate: "#86efac",
54+
expert: "#fde047",
55+
};
56+
</script>
57+
58+
<ClientOnly>
59+
<VueSelect
60+
v-model="selected"
61+
:is-multi="true"
62+
:options="skillOptions"
63+
placeholder="Select your skills"
64+
>
65+
<template #tag-content="{ option }">
66+
<span :style="{ display: 'flex', alignItems: 'center', gap: '4px' }">
67+
<span>{{ option.icon }}</span>
68+
<strong>{{ option.label }}</strong>
69+
<span
70+
:style="{
71+
fontSize: '10px',
72+
padding: '2px 4px',
73+
borderRadius: '2px',
74+
backgroundColor: levelColors[option.level],
75+
color: '#000',
76+
fontWeight: 500,
77+
}"
78+
>
79+
{{ option.level }}
80+
</span>
81+
</span>
82+
</template>
83+
</VueSelect>
84+
</ClientOnly>
85+
86+
## Demo source-code
87+
88+
```vue
89+
<script setup>
90+
import { ref } from "vue";
91+
import VueSelect from "vue3-select-component";
92+
93+
const selected = ref([]);
94+
95+
const skillOptions = [
96+
{ label: "JavaScript", value: "javascript", level: "expert", icon: "🟨" },
97+
{ label: "TypeScript", value: "typescript", level: "expert", icon: "🔷" },
98+
{ label: "Vue.js", value: "vue", level: "expert", icon: "💚" },
99+
{ label: "React", value: "react", level: "intermediate", icon: "⚛️" },
100+
{ label: "Python", value: "python", level: "beginner", icon: "🐍" },
101+
{ label: "Rust", value: "rust", level: "beginner", icon: "🦀" },
102+
];
103+
104+
const levelColors = {
105+
beginner: "#93c5fd",
106+
intermediate: "#86efac",
107+
expert: "#fde047",
108+
};
109+
</script>
110+
111+
<template>
112+
<VueSelect
113+
v-model="selected"
114+
:is-multi="true"
115+
:options="skillOptions"
116+
placeholder="Select your skills"
117+
>
118+
<template #tag-content="{ option }">
119+
<span :style="{ display: 'flex', alignItems: 'center', gap: '4px' }">
120+
<span>{{ option.icon }}</span>
121+
<strong>{{ option.label }}</strong>
122+
<span
123+
:style="{
124+
fontSize: '10px',
125+
padding: '2px 4px',
126+
borderRadius: '2px',
127+
backgroundColor: levelColors[option.level],
128+
color: '#000',
129+
fontWeight: 500,
130+
}"
131+
>
132+
{{ option.level }}
133+
</span>
134+
</span>
135+
</template>
136+
</VueSelect>
137+
</template>
138+
```
139+
140+
## Key Benefits
141+
142+
1. **Simpler Implementation**: No need to recreate the tag structure or remove button
143+
2. **Consistent Styling**: Automatically inherits all CSS variables for tags
144+
3. **Type Safety**: Full TypeScript support with option typing
145+
4. **Less Code**: Focus only on the content formatting
146+
5. **Maintainability**: Future component updates automatically apply
147+
148+
## Comparison: `#tag` vs `#tag-content`
149+
150+
### With `#tag-content` (Simpler)
151+
152+
```html
153+
<template #tag-content="{ option }">
154+
<strong>{{ option.label }}</strong>
155+
</template>
156+
```
157+
158+
The component handles:
159+
- Tag wrapper (`<div class="multi-value">`)
160+
- Remove button with icon
161+
- Click handlers
162+
- Styling with CSS variables
163+
164+
### With `#tag` (Full Control)
165+
166+
```html
167+
<template #tag="{ option, removeOption }">
168+
<div class="custom-tag">
169+
<strong>{{ option.label }}</strong>
170+
<button @click="removeOption">×</button>
171+
</div>
172+
</template>
173+
```
174+
175+
You must handle:
176+
- Complete tag structure
177+
- Custom button and event handler
178+
- All styling from scratch

docs/slots.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Customize the rendered template if a selected option (inside the select control)
5858

5959
When using `isMulti` prop, customize the rendered template of a selected option. You can use the slot props to retrieve the current selected option and a function to remove it.
6060

61+
This slot gives you complete control over the tag structure, styling, and behavior. You are responsible for implementing the remove button and its event handler.
62+
6163
```vue
6264
<template>
6365
<VueSelect
@@ -72,6 +74,48 @@ When using `isMulti` prop, customize the rendered template of a selected option.
7274
</template>
7375
```
7476

77+
::: tip
78+
If you only want to customize the text inside a tag while keeping the default structure, styling, and remove button, consider using the `#tag-content` slot instead. See below.
79+
:::
80+
81+
## tag-content
82+
83+
**Type**: `slotProps: { option: Option }`
84+
85+
When using `isMulti` prop, customize only the text content inside a tag, while keeping the default tag structure, styling, and remove button.
86+
87+
This is a simpler alternative to the `#tag` slot when you only need to format or enhance the label text without recreating the entire tag structure.
88+
89+
```vue
90+
<template>
91+
<VueSelect
92+
v-model="option"
93+
:options="options"
94+
:is-multi="true"
95+
>
96+
<template #tag-content="{ option }">
97+
<strong>{{ option.label }}</strong>
98+
</template>
99+
</VueSelect>
100+
</template>
101+
```
102+
103+
::: info
104+
**When to use `#tag-content` vs `#tag`:**
105+
106+
Use `#tag-content` when:
107+
- You only want to format or transform the label text
108+
- You want to add icons, badges, or emphasis to the text
109+
- You want to keep the default tag styling and remove button
110+
111+
Use `#tag` when:
112+
- You need complete control over the tag structure
113+
- You want custom styling that can't be achieved with CSS variables
114+
- You need to change the remove button appearance or behavior
115+
116+
**Note:** If both slots are provided, `#tag` takes priority and `#tag-content` is ignored.
117+
:::
118+
75119
## menu-header
76120

77121
**Type**: `slotProps: {}`

playground/PlaygroundLayout.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const links = [
99
{ value: "/multi-select", label: "Multi Select" },
1010
{ value: "/multi-select-taggable", label: "Multi Select Taggable" },
1111
{ value: "/custom-placeholder", label: "Custom Placeholder" },
12+
{ value: "/custom-tag-content", label: "Custom Tag Content" },
1213
{ value: "/extra-option-properties", label: "Extra Option Properties" },
1314
{ value: "/custom-option-label-value", label: "Custom Option Label/Value" },
1415
{ value: "/custom-search-filter", label: "Custom Search Filter" },
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<script setup lang="ts">
2+
import type { Option } from "../../src";
3+
import { ref } from "vue";
4+
import VueSelect from "../../src";
5+
6+
const selected = ref<string[]>([]);
7+
8+
type SkillOption = {
9+
level: "beginner" | "intermediate" | "expert";
10+
icon: string;
11+
} & Option<string>;
12+
13+
const options = ref<SkillOption[]>([
14+
{ label: "JavaScript", value: "javascript", level: "expert", icon: "🟨" },
15+
{ label: "TypeScript", value: "typescript", level: "expert", icon: "🔷" },
16+
{ label: "Vue.js", value: "vue", level: "expert", icon: "💚" },
17+
{ label: "React", value: "react", level: "intermediate", icon: "⚛️" },
18+
{ label: "Python", value: "python", level: "beginner", icon: "🐍" },
19+
{ label: "Rust", value: "rust", level: "beginner", icon: "🦀" },
20+
]);
21+
22+
const levelColors = {
23+
beginner: "#93c5fd",
24+
intermediate: "#86efac",
25+
expert: "#fde047",
26+
};
27+
</script>
28+
29+
<template>
30+
<div>
31+
<h2>Custom Tag Content Slot</h2>
32+
<p>
33+
This demo shows the <code>#tag-content</code> slot which allows you to customize
34+
only the text inside a tag, while keeping the default structure, styling, and remove button.
35+
</p>
36+
37+
<VueSelect
38+
v-model="selected"
39+
:options="options"
40+
:is-multi="true"
41+
placeholder="Select your skills"
42+
>
43+
<template #tag-content="{ option }">
44+
<span :style="{ display: 'flex', alignItems: 'center', gap: '4px' }">
45+
<span>{{ option.icon }}</span>
46+
<strong>{{ option.label }}</strong>
47+
<span
48+
:style="{
49+
fontSize: '10px',
50+
padding: '2px 4px',
51+
borderRadius: '2px',
52+
backgroundColor: levelColors[option.level],
53+
color: '#000',
54+
fontWeight: 500,
55+
}"
56+
>
57+
{{ option.level }}
58+
</span>
59+
</span>
60+
</template>
61+
</VueSelect>
62+
63+
<div style="margin-top: 20px">
64+
<h3>Selected Skills:</h3>
65+
<pre>{{ selected.length ? selected : "none" }}</pre>
66+
</div>
67+
68+
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 4px">
69+
<h3>Why use #tag-content instead of #tag?</h3>
70+
<ul>
71+
<li><strong>#tag-content</strong>: Only replaces the text inside the tag. The wrapper, styling, and remove button are preserved automatically.</li>
72+
<li><strong>#tag</strong>: Replaces the entire tag structure. You need to recreate the wrapper, styling, and remove button manually.</li>
73+
</ul>
74+
<p>Use <code>#tag-content</code> when you just want to format the label text differently without dealing with structure and event handlers.</p>
75+
</div>
76+
</div>
77+
</template>
78+
79+
<style scoped>
80+
h2 {
81+
margin-bottom: 10px;
82+
}
83+
84+
p {
85+
margin-bottom: 20px;
86+
color: #666;
87+
}
88+
89+
code {
90+
background: #f0f0f0;
91+
padding: 2px 6px;
92+
border-radius: 3px;
93+
font-family: monospace;
94+
}
95+
96+
pre {
97+
background: #f9f9f9;
98+
padding: 10px;
99+
border-radius: 4px;
100+
overflow-x: auto;
101+
}
102+
</style>

playground/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ControlledMenu from "./demos/ControlledMenu.vue";
55
import CustomOptionLabelValue from "./demos/CustomOptionLabelValue.vue";
66
import CustomPlaceholder from "./demos/CustomPlaceholder.vue";
77
import CustomSearchFilter from "./demos/CustomSearchFilter.vue";
8+
import CustomTagContent from "./demos/CustomTagContent.vue";
89
import ExtraOptionProperties from "./demos/ExtraOptionProperties.vue";
910
import MenuHeader from "./demos/MenuHeader.vue";
1011
import MenuPositioning from "./demos/MenuPositioning.vue";
@@ -23,6 +24,7 @@ const router = createRouter({
2324
{ path: "/multi-select", component: MultiSelect },
2425
{ path: "/multi-select-taggable", component: MultiSelectTaggable },
2526
{ path: "/custom-placeholder", component: CustomPlaceholder },
27+
{ path: "/custom-tag-content", component: CustomTagContent },
2628
{ path: "/extra-option-properties", component: ExtraOptionProperties },
2729
{ path: "/custom-option-label-value", component: CustomOptionLabelValue },
2830
{ path: "/custom-search-filter", component: CustomSearchFilter },

src/MultiValue.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
<script setup lang="ts">
1+
<script setup lang="ts" generic="GenericOption">
22
import XMarkIcon from "./icons/XMarkIcon.vue";
33
44
const props = defineProps<{
55
label: string;
6+
option: GenericOption;
67
classes?: {
78
multiValue?: string;
89
multiValueLabel?: string;
910
multiValueRemove?: string;
1011
};
12+
tagContentSlot?: (props: { option: GenericOption }) => any;
1113
}>();
1214
1315
const emit = defineEmits<{
@@ -21,7 +23,13 @@ const emit = defineEmits<{
2123
:class="props.classes?.multiValue"
2224
>
2325
<div class="multi-value-label" :class="props.classes?.multiValueLabel">
24-
{{ props.label }}
26+
<template v-if="props.tagContentSlot">
27+
<component :is="props.tagContentSlot" :option="props.option" />
28+
</template>
29+
30+
<template v-else>
31+
{{ props.label }}
32+
</template>
2533
</div>
2634

2735
<button

0 commit comments

Comments
 (0)