Skip to content

Commit 745c590

Browse files
committed
feat(MultiSelect): add support for custom templates
1 parent 40fafca commit 745c590

File tree

4 files changed

+208
-21
lines changed

4 files changed

+208
-21
lines changed

docs/assets/js/partials/snippets.js

Lines changed: 129 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -397,22 +397,27 @@ export default () => {
397397
name: 'multiSelect',
398398
options: [
399399
{
400-
value: 0,
401-
text: 'Angular'
402-
},
403-
{
404-
value: 1,
405-
text: 'Bootstrap',
406-
selected: true
407-
},
408-
{
409-
value: 2,
410-
text: 'React.js',
411-
selected: true
412-
},
413-
{
414-
value: 3,
415-
text: 'Vue.js'
400+
label: 'frontend',
401+
options: [
402+
{
403+
value: 0,
404+
text: 'Angular'
405+
},
406+
{
407+
value: 1,
408+
text: 'Bootstrap',
409+
selected: true
410+
},
411+
{
412+
value: 2,
413+
text: 'React.js',
414+
selected: true
415+
},
416+
{
417+
value: 3,
418+
text: 'Vue.js'
419+
}
420+
]
416421
},
417422
{
418423
label: 'backend',
@@ -438,6 +443,114 @@ export default () => {
438443
}
439444
// js-docs-end multi-select-array-data
440445

446+
// js-docs-start multi-select-custom-options
447+
const myMultiSelectCountries = document.getElementById('myMultiSelectCountries')
448+
const myMultiSelectCountriesAndCities = document.getElementById('myMultiSelectCountriesAndCities')
449+
450+
if (myMultiSelectCountries && myMultiSelectCountriesAndCities) {
451+
const countries = [
452+
{
453+
value: 'pl',
454+
text: 'Poland',
455+
flag: '🇵🇱'
456+
},
457+
{
458+
value: 'de',
459+
text: 'Germany',
460+
flag: '🇩🇪'
461+
},
462+
{
463+
value: 'us',
464+
text: 'United States',
465+
flag: '🇺🇸'
466+
},
467+
{
468+
value: 'es',
469+
text: 'Spain',
470+
flag: '🇪🇸'
471+
},
472+
{
473+
value: 'gb',
474+
text: 'United Kingdom',
475+
flag: '🇬🇧'
476+
}
477+
]
478+
479+
const cities = [
480+
{
481+
label: 'United States',
482+
code: 'us',
483+
flag: '🇺🇸',
484+
options: [
485+
{
486+
value: 'au',
487+
text: 'Austin'
488+
},
489+
{
490+
value: 'ch',
491+
text: 'Chicago'
492+
},
493+
{
494+
value: 'la',
495+
text: 'Los Angeles'
496+
},
497+
{
498+
value: 'ny',
499+
text: 'New York'
500+
},
501+
{
502+
value: 'sa',
503+
text: 'San Jose'
504+
}
505+
]
506+
},
507+
{
508+
label: 'United Kingdom',
509+
code: 'gb',
510+
flag: '🇬🇧',
511+
options: [
512+
{
513+
value: 'li',
514+
text: 'Liverpool'
515+
},
516+
{
517+
value: 'lo',
518+
text: 'London'
519+
},
520+
{
521+
value: 'ma',
522+
text: 'Manchester'
523+
}
524+
]
525+
}
526+
]
527+
528+
new coreui.MultiSelect(myMultiSelectCountries, {
529+
cleaner: true,
530+
multiple: true,
531+
options: countries,
532+
optionsTemplate(option) {
533+
return `<div class="d-flex align-items-center gap-2"><span class="fs-5">${option.flag}</span><span>${option.text}</span></div>`
534+
},
535+
placeholder: 'Select countries',
536+
search: true,
537+
selectionType: 'tags'
538+
})
539+
540+
new coreui.MultiSelect(myMultiSelectCountriesAndCities, {
541+
cleaner: true,
542+
multiple: true,
543+
options: cities,
544+
optionsGroupsTemplate(optionGroup) {
545+
return `<div class="d-flex align-items-center gap-2"><span class="text-body fs-5">${optionGroup.flag}</span><span>${optionGroup.label}</span></div>`
546+
},
547+
placeholder: 'Select cities',
548+
search: true,
549+
selectionType: 'tags'
550+
})
551+
}
552+
// js-docs-end multi-select-custom-options
553+
441554
// -------------------------------
442555
// Calendars
443556
// -------------------------------

docs/content/forms/multi-select.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,25 @@ Add the `data-coreui-disabled="true"` boolean attribute to give it a grayed out
228228
</select>
229229
{{< /example >}}
230230

231+
## Custom templates
232+
233+
The CoreUI Bootstrap Multi Select Component provides the flexibility to personalize options and group labels by utilizing custom templates. You can easily customize the options using the `optionsTemplate`, and for groups, you can use `optionsGroupsTemplate`, as demonstrated in the examples below:
234+
235+
{{< example stackblitz_pro="true" stackblitz_add_js="true">}}
236+
<div class="row">
237+
<div class="col-md-6">
238+
<select id="myMultiSelectCountries" class="form-multi-select"></select>
239+
</div>
240+
<div class="col-md-6">
241+
<select id="myMultiSelectCountriesAndCities" class="form-multi-select"></select>
242+
</div>
243+
</div>
244+
{{< /example >}}
245+
246+
We use the following JavaScript to set up our multi-select:
247+
248+
{{< js-docs name="multi-select-custom-options" file="docs/assets/js/partials/snippets.js" >}}
249+
231250
## Sizing
232251

233252
You may also choose from small and large multi selects to match our similarly sized text inputs.
@@ -333,6 +352,7 @@ const mulitSelectList = mulitSelectElementList.map(mulitSelectEl => {
333352
{{< bs-table >}}
334353
| Name | Type | Default | Description |
335354
| --- | --- | --- | --- |
355+
| `allowList` | object | `DefaultAllowlist` | Object containing allowed tags and attributes for HTML sanitization when using custom templates. |
336356
| `ariaCleanerLabel`| string | `Clear all selections` | A string that provides an accessible label for the cleaner button. This label is read by screen readers to describe the action associated with the button. |
337357
| `cleaner`| boolean | `true` | Enables selection cleaner element. |
338358
| `clearSearchOnSelect`| boolean | `false` | Clear current search on selecting an item. |
@@ -342,9 +362,14 @@ const mulitSelectList = mulitSelectElementList.map(mulitSelectEl => {
342362
| `multiple` | boolean | `true` | It specifies that multiple options can be selected at once. |
343363
| `name` | string, null | `null` | Set the name attribute for the native select element. |
344364
| `options` | boolean, array | `false` | List of option elements. |
365+
| `optionsGroupsTemplate` | function, null | `null` | Custom template function for rendering option group labels. Receives the group object as parameter. |
345366
| `optionsMaxHeight` | number, string | `'auto'` | Sets `max-height` of options list. |
346367
| `optionsStyle` | string | `'checkbox'` | Sets option style. |
368+
| `optionsTemplate` | function, null | `null` | Custom template function for rendering individual options. Receives the option object as parameter. |
347369
| `placeholder` | string | `'Select...'` | Specifies a short hint that is visible in the input. |
370+
| `required` | boolean | `false` | Makes the input field required for form validation. |
371+
| `sanitize` | boolean | `true` | Enables HTML sanitization for custom templates to prevent XSS attacks. |
372+
| `sanitizeFn` | function, null | `null` | Custom sanitization function. If provided, it will be used instead of the built-in sanitizer. |
348373
| `search` | boolean, string | `false` | Enables search input element. When set to `'global'`, the user can perform searches across the entire component, regardless of where their focus is within the component. |
349374
| `searchNoResultsLabel` | string | `'No results found'` | Sets the label for no results when filtering. |
350375
| `selectAll` | boolean | `true` | Enables select all button.|

js/src/multi-select.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import BaseComponent from './base-component.js'
1010
import Data from './dom/data.js'
1111
import EventHandler from './dom/event-handler.js'
1212
import SelectorEngine from './dom/selector-engine.js'
13+
import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer.js'
1314
import {
1415
defineJQueryPlugin,
1516
getNextActiveElement,
@@ -83,6 +84,7 @@ const CLASS_NAME_TAG = 'form-multi-select-tag'
8384
const CLASS_NAME_TAG_DELETE = 'form-multi-select-tag-delete'
8485

8586
const Default = {
87+
allowList: DefaultAllowlist,
8688
ariaCleanerLabel: 'Clear all selections',
8789
cleaner: true,
8890
clearSearchOnSelect: false,
@@ -92,10 +94,14 @@ const Default = {
9294
multiple: true,
9395
name: null,
9496
options: false,
97+
optionsGroupsTemplate: null,
9598
optionsMaxHeight: 'auto',
9699
optionsStyle: 'checkbox',
100+
optionsTemplate: null,
97101
placeholder: 'Select...',
98102
required: false,
103+
sanitize: true,
104+
sanitizeFn: null,
99105
search: false,
100106
searchNoResultsLabel: 'No results found',
101107
selectAll: true,
@@ -107,6 +113,7 @@ const Default = {
107113
}
108114

109115
const DefaultType = {
116+
allowList: 'object',
110117
ariaCleanerLabel: 'string',
111118
cleaner: 'boolean',
112119
clearSearchOnSelect: 'boolean',
@@ -116,10 +123,14 @@ const DefaultType = {
116123
multiple: 'boolean',
117124
name: '(string|null)',
118125
options: '(boolean|array)',
126+
optionsGroupsTemplate: '(function|null)',
119127
optionsMaxHeight: '(number|string)',
120128
optionsStyle: 'string',
129+
optionsTemplate: '(function|null)',
121130
placeholder: 'string',
122131
required: 'boolean',
132+
sanitize: 'boolean',
133+
sanitizeFn: '(null|function)',
123134
search: '(boolean|string)',
124135
searchNoResultsLabel: 'string',
125136
selectAll: 'boolean',
@@ -407,18 +418,31 @@ class MultiSelect extends BaseComponent {
407418
const _options = []
408419
for (const option of options) {
409420
if (option.options && Array.isArray(option.options)) {
421+
const customGroupProperties = { ...option }
422+
423+
delete customGroupProperties.label
424+
delete customGroupProperties.options
425+
410426
_options.push({
427+
...customGroupProperties,
411428
label: option.label,
412429
options: this._getOptionsFromConfig(option.options)
413430
})
431+
414432
continue
415433
}
416434

417435
const value = String(option.value)
418436
const isSelected = option.selected || (this._config.value && this._config.value.includes(value))
419437

438+
const customProperties = typeof option === 'object' ? { ...option } : {}
439+
440+
delete customProperties.value
441+
delete customProperties.selected
442+
delete customProperties.disabled
443+
420444
_options.push({
421-
...option,
445+
...customProperties,
422446
value,
423447
...isSelected && { selected: true },
424448
...option.disabled && { disabled: true }
@@ -691,7 +715,15 @@ class MultiSelect extends BaseComponent {
691715

692716
optionDiv.dataset.value = String(option.value)
693717
optionDiv.tabIndex = 0
694-
optionDiv.innerHTML = option.text
718+
719+
if (this._config.optionsTemplate && typeof this._config.optionsTemplate === 'function') {
720+
optionDiv.innerHTML = this._config.sanitize ?
721+
sanitizeHtml(this._config.optionsTemplate(option), this._config.allowList, this._config.sanitizeFn) :
722+
this._config.optionsTemplate(option)
723+
} else {
724+
optionDiv.textContent = option.text
725+
}
726+
695727
parentElement.append(optionDiv)
696728
}
697729

@@ -700,7 +732,15 @@ class MultiSelect extends BaseComponent {
700732
optgroup.classList.add(CLASS_NAME_OPTGROUP)
701733

702734
const optgrouplabel = document.createElement('div')
703-
optgrouplabel.innerHTML = option.label
735+
736+
if (this._config.optionsGroupsTemplate && typeof this._config.optionsGroupsTemplate === 'function') {
737+
optgrouplabel.innerHTML = this._config.sanitize ?
738+
sanitizeHtml(this._config.optionsGroupsTemplate(option), this._config.allowList, this._config.sanitizeFn) :
739+
this._config.optionsGroupsTemplate(option)
740+
} else {
741+
optgrouplabel.textContent = option.label
742+
}
743+
704744
optgrouplabel.classList.add(CLASS_NAME_OPTGROUP_LABEL)
705745
optgroup.append(optgrouplabel)
706746

@@ -737,10 +777,18 @@ class MultiSelect extends BaseComponent {
737777
}
738778

739779
_onOptionsClick(element) {
740-
if (!element.classList.contains(CLASS_NAME_OPTION) || element.classList.contains(CLASS_NAME_LABEL)) {
780+
if (element.classList.contains(CLASS_NAME_LABEL)) {
741781
return
742782
}
743783

784+
if (!element.classList.contains(CLASS_NAME_OPTION)) {
785+
element = element.closest(SELECTOR_OPTION)
786+
787+
if (!element) {
788+
return
789+
}
790+
}
791+
744792
const value = String(element.dataset.value)
745793
const { text } = this._findOptionByValue(value)
746794

scss/forms/_form-multi-select.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ select.form-multi-select {
414414
&::before {
415415
position: absolute;
416416
inset-inline-start: calc(var(--#{$prefix}form-multi-select-option-padding-x) * .5); // stylelint-disable-line function-disallowed-list
417-
top: .7rem;
417+
top: 50%;
418418
display: block;
419419
width: var(--#{$prefix}form-multi-select-option-indicator-width);
420420
height: var(--#{$prefix}form-multi-select-option-indicator-width);
@@ -425,6 +425,7 @@ select.form-multi-select {
425425
background-position: center;
426426
background-size: contain;
427427
border: var(--#{$prefix}form-multi-select-option-indicator-border);
428+
transform: translateY(-50%);
428429
@include border-radius(var(--#{$prefix}form-multi-select-option-indicator-border-radius));
429430
}
430431
}

0 commit comments

Comments
 (0)