From 276465aa9d5eb7a0bc1459d6dfd7cf3f8b1bdd7a Mon Sep 17 00:00:00 2001 From: Enrico Sacchetti Date: Wed, 25 Oct 2023 21:56:52 -0400 Subject: [PATCH] feat(MultiSelect): add selectedOnly and combineValues props The MultiSelect wasn't rendering form-submittable inputs. This fix adds two new props to help customize how the hidden inputs render. See #1742 --- COMPONENT_INDEX.md | 77 ++++++++++++----------- docs/src/COMPONENT_API.json | 28 ++++++++- docs/src/pages/components/MultiSelect.svx | 60 +++++++++++++++++- src/MultiSelect/MultiSelect.svelte | 29 ++++++++- types/MultiSelect/MultiSelect.svelte.d.ts | 17 ++++- 5 files changed, 167 insertions(+), 44 deletions(-) diff --git a/COMPONENT_INDEX.md b/COMPONENT_INDEX.md index fff753bc71..fc9730028d 100644 --- a/COMPONENT_INDEX.md +++ b/COMPONENT_INDEX.md @@ -2355,43 +2355,46 @@ export interface MultiSelectItem { ### Props -| Prop name | Required | Kind | Reactive | Type | Default value | Description | -| :----------------------- | :------- | :--------------- | :------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| highlightedId | No | let | Yes | null | MultiSelectItemId | null | Id of the highlighted ListBoxMenuItem | -| selectionRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the selection element | -| fieldRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the field box element | -| multiSelectRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the outer div element | -| inputRef | No | let | Yes | null | HTMLInputElement | null | Obtain a reference to the input HTML element | -| open | No | let | Yes | boolean | false | Set to `true` to open the dropdown | -| value | No | let | Yes | string | "" | Specify the multiselect value | -| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the selected ids | -| items | No | let | Yes | ReadonlyArray | [] | Set the multiselect items | -| itemToString | No | let | No | (item: MultiSelectItem) => any | (item) => item.text || item.id | Override the display of a multiselect item | -| itemToInput | No | let | No | (item: MultiSelectItem) => { name?: string; labelText?: any; title?: string; } | (item) => {} | Override the item name, title, labelText passed to the checkbox input | -| size | No | let | No | "sm" | "lg" | "xl" | undefined | Set the size of the combobox | -| type | No | let | No | "default" | "inline" | "default" | Specify the type of multiselect | -| direction | No | let | No | "bottom" | "top" | "bottom" | Specify the direction of the multiselect dropdown menu | -| selectionFeedback | No | let | No | "top" | "fixed" | "top-after-reopen" | "top-after-reopen" | Specify the selection feedback after selecting items | -| disabled | No | let | No | boolean | false | Set to `true` to disable the dropdown | -| filterable | No | let | No | boolean | false | Set to `true` to filter items | -| filterItem | No | let | No | (item: MultiSelectItem, value: string) => string | (item, value) => item.text.toLowerCase().includes(value.trim().toLowerCase()) | Override the filtering logic
The default filtering is an exact string comparison | -| light | No | let | No | boolean | false | Set to `true` to enable the light variant | -| locale | No | let | No | string | "en" | Specify the locale | -| placeholder | No | let | No | string | "" | Specify the placeholder text | -| sortItem | No | let | No | ((a: MultiSelectItem, b: MultiSelectItem) => MultiSelectItem) | (() => void) | (a, b) => a.text.localeCompare(b.text, locale, { numeric: true }) | Override the sorting logic
The default sorting compare the item text value | -| translateWithId | No | let | No | (id: import("../ListBox/ListBoxMenuIcon.svelte").ListBoxMenuIconTranslationId) => string | undefined | Override the chevron icon label based on the open state.
Defaults to "Open menu" when closed and "Close menu" when open | -| translateWithIdSelection | No | let | No | (id: import("../ListBox/ListBoxSelection.svelte").ListBoxSelectionTranslationId) => string | undefined | Override the label of the clear button when the input has a selection.
Defaults to "Clear selected item" and "Clear all items" if more than one item is selected | -| titleText | No | let | No | string | "" | Specify the title text | -| useTitleInItem | No | let | No | boolean | false | Set to `true` to pass the item to `itemToString` in the checkbox | -| invalid | No | let | No | boolean | false | Set to `true` to indicate an invalid state | -| invalidText | No | let | No | string | "" | Specify the invalid state text | -| warn | No | let | No | boolean | false | Set to `true` to indicate an warning state | -| warnText | No | let | No | string | "" | Specify the warning state text | -| helperText | No | let | No | string | "" | Specify the helper text | -| label | No | let | No | string | "" | Specify the list box label | -| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text | -| id | No | let | No | string | "ccs-" + Math.random().toString(36) | Set an id for the list box component | -| name | No | let | No | string | undefined | Specify a name attribute for the select | +| Prop name | Required | Kind | Reactive | Type | Default value | Description | +| :------------------------------ | :------------------------ | :---------------------------------------------------------------------------------------------------------------------------- | :------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| highlightedId | No | let | Yes | null | MultiSelectItemId | null | Id of the highlighted ListBoxMenuItem | +| selectionRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the selection element | +| fieldRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the field box element | +| multiSelectRef | No | let | Yes | null | HTMLDivElement | null | Obtain a reference to the outer div element | +| inputRef | No | let | Yes | null | HTMLInputElement | null | Obtain a reference to the input HTML element | +| open | No | let | Yes | boolean | false | Set to `true` to open the dropdown | +| value | No | let | Yes | string | "" | Specify the multiselect value | +| selectedIds | No | let | Yes | ReadonlyArray | [] | Set the selected ids | +| items | No | let | Yes | ReadonlyArray | [] | Set the multiselect items | +| itemToString | No | let | No | (item: MultiSelectItem) => any | (item) => item.text || item.id | Override the display of a multiselect item | +| itemToInput | No | let | No | (item: MultiSelectItem) => { name?: string; labelText?: any; title?: | +| string; value?: string } | (item) => {} | Override the item name, title, labelText, or value passed to the user-selectable checkbox input as well as the hidden inputs. | +| selectedOnly | No | let | No | boolean | false | Set to `true` to only render selected options as hidden inputs for form submission. | +| combineValues | No | let | No | false | true | string | false | Combine selected items as comma-separated values when submitted in a form.
If set to `true`, the default separator is a comma `,`.
Pass in a string to override the separator. | +| size | No | let | No | "sm" | "lg" | "xl" | undefined | Set the size of the combobox | +| type | No | let | No | "default" | "inline" | "default" | Specify the type of multiselect | +| direction | No | let | No | "bottom" | "top" | "bottom" | Specify the direction of the multiselect dropdown menu | +| selectionFeedback | No | let | No | "top" | "fixed" | "top-after-reopen" | "top-after-reopen" | Specify the selection feedback after selecting items | +| disabled | No | let | No | boolean | false | Set to `true` to disable the dropdown | +| filterable | No | let | No | boolean | false | Set to `true` to filter items | +| filterItem | No | let | No | (item: MultiSelectItem, value: string) => string | (item, value) => item.text.toLowerCase().includes(value.trim().toLowerCase()) | Override the filtering logic
The default filtering is an exact string comparison | +| light | No | let | No | boolean | false | Set to `true` to enable the light variant | +| locale | No | let | No | string | "en" | Specify the locale | +| placeholder | No | let | No | string | "" | Specify the placeholder text | +| sortItem | No | let | No | ((a: MultiSelectItem, b: MultiSelectItem) => MultiSelectItem) | (() => void) | (a, b) => a.text.localeCompare(b.text, locale, { numeric: true }) | Override the sorting logic
The default sorting compare the item text value | +| translateWithId | No | let | No | (id: import("../ListBox/ListBoxMenuIcon.svelte").ListBoxMenuIconTranslationId) => string | undefined | Override the chevron icon label based on the open state.
Defaults to "Open menu" when closed and "Close menu" when open | +| translateWithIdSelection | No | let | No | (id: import("../ListBox/ListBoxSelection.svelte").ListBoxSelectionTranslationId) => string | undefined | Override the label of the clear button when the input has a selection.
Defaults to "Clear selected item" and "Clear all items" if more than one item is selected | +| titleText | No | let | No | string | "" | Specify the title text | +| useTitleInItem | No | let | No | boolean | false | Set to `true` to pass the item to `itemToString` in the checkbox | +| invalid | No | let | No | boolean | false | Set to `true` to indicate an invalid state | +| invalidText | No | let | No | string | "" | Specify the invalid state text | +| warn | No | let | No | boolean | false | Set to `true` to indicate an warning state | +| warnText | No | let | No | string | "" | Specify the warning state text | +| helperText | No | let | No | string | "" | Specify the helper text | +| label | No | let | No | string | "" | Specify the list box label | +| hideLabel | No | let | No | boolean | false | Set to `true` to visually hide the label text | +| id | No | let | No | string | "ccs-" + Math.random().toString(36) | Set an id for the list box component | +| name | No | let | No | string | undefined | Specify a name attribute for the select | ### Slots diff --git a/docs/src/COMPONENT_API.json b/docs/src/COMPONENT_API.json index cf5a36521d..1cbcc58e10 100644 --- a/docs/src/COMPONENT_API.json +++ b/docs/src/COMPONENT_API.json @@ -7006,8 +7006,8 @@ { "name": "itemToInput", "kind": "let", - "description": "Override the item name, title, labelText passed to the checkbox input", - "type": "(item: MultiSelectItem) => { name?: string; labelText?: any; title?: string; }", + "description": "Override the item name, title, labelText, or value passed to the user-selectable checkbox input as well as the hidden inputs.", + "type": "(item: MultiSelectItem) => { name?: string; labelText?: any; title?:\nstring; value?: string }", "value": "(item) => {}", "isFunction": true, "isFunctionDeclaration": false, @@ -7015,6 +7015,30 @@ "constant": false, "reactive": false }, + { + "name": "selectedOnly", + "kind": "let", + "description": "Set to `true` to only render selected options as hidden inputs for form submission.", + "type": "boolean", + "value": "false", + "isFunction": false, + "isFunctionDeclaration": false, + "isRequired": false, + "constant": false, + "reactive": false + }, + { + "name": "combineValues", + "kind": "let", + "description": "Combine selected items as comma-separated values when submitted in a form.\nIf set to `true`, the default separator is a comma `,`.\nPass in a string to override the separator.", + "type": "false | true | string", + "value": "false", + "isFunction": false, + "isFunctionDeclaration": false, + "isRequired": false, + "constant": false, + "reactive": false + }, { "name": "selectedIds", "kind": "let", diff --git a/docs/src/pages/components/MultiSelect.svx b/docs/src/pages/components/MultiSelect.svx index 35387f645e..fde130b3d6 100644 --- a/docs/src/pages/components/MultiSelect.svx +++ b/docs/src/pages/components/MultiSelect.svx @@ -11,7 +11,13 @@ ## Default -By default, items will be ordered alphabetically based on the `item.text` value. To prevent this, see [#no-alphabetical-ordering](#no-alphabetical-ordering). +By default, items will be ordered alphabetically based on the `item.text` value. +To prevent this, see [#no-alphabetical-ordering](#no-alphabetical-ordering). + +Hidden inputs will be rendered based on user selection, such as `` to mirror checkbox values and to allow MultiSelect to +be submittable within forms. These hidden inputs can be customized with +the `combineValues` or `itemToInput` props. +## Format checkbox values + +Use the `itemToInput` prop to customize the user-selectable checkbox values. +This will also override the underlying hidden inputs used for form submission. + +For example: + +```js +(item) => ({name: `Contact_${item.id}`], value: item.id}) + + +``` + +The above function sets the `name` attribute to +`Contact_0` (respective to each item's `id`) for every hidden input that +renders, along with each respective item's `id` set to the `value` attribute. + +When using with the `selectedOnly` prop, you can override every hidden input to +use the same `name` attribute. This may be useful if you +wish to use `formData.getAll('contact')` in your server-side form handler. + + ({name: 'contact', value: item.id})} + selectedOnly + titleText="Contact" + label="Select contact methods..." + items="{[ + {id: "0", text: "Slack"}, + {id: "1", text: "Email"}, + {id: "2", text: "Fax"} + ]}" +/> + +## Combine values into a single input + +With the `combineValues` prop, all selected items' values will render as a +comma-separated string within a single hidden input. A custom delimiter can +alternatively be passed in. + + + ## Top direction Set `direction` to `"top"` for the dropdown menu to appear above the input. @@ -159,4 +215,4 @@ Use the `disabled` property in the `items` prop to disable specific items. { id: "1", text: "Email", disabled: true }, { id: "2", text: "Fax" }, ]} -/> \ No newline at end of file +/> diff --git a/src/MultiSelect/MultiSelect.svelte b/src/MultiSelect/MultiSelect.svelte index b78cd531e3..173fe70bf7 100644 --- a/src/MultiSelect/MultiSelect.svelte +++ b/src/MultiSelect/MultiSelect.svelte @@ -22,11 +22,26 @@ export let itemToString = (item) => item.text || item.id; /** - * Override the item name, title, labelText passed to the checkbox input - * @type {(item: MultiSelectItem) => { name?: string; labelText?: any; title?: string; }} + * Override the item name, title, labelText, or value passed to the user-selectable checkbox input as well as the hidden inputs. + * @type {(item: MultiSelectItem) => { name?: string; labelText?: any; title?: + * string; value?: string }} */ export let itemToInput = (item) => {}; + /** + * Set to `true` to only render selected options as hidden inputs for form submission. + * @type {boolean} + */ + export let selectedOnly = false + + /** + * Combine selected items as comma-separated values when submitted in a form. + * If set to `true`, the default separator is a comma `,`. + * Pass in a string to override the separator. + * @type {false | true | string} + */ + export let combineValues = false + /** * Set the selected ids * @type {ReadonlyArray} @@ -533,6 +548,16 @@ {/each} + {:else} + {#if combineValues} + {@const items = checked.map(el => el.id).join(typeof combineValues === + 'string' ? combineValues : ',')} + + {:else} + {#each selectedOnly ? checked : sortedItems as item (item.id)} + + {/each} + {/if} {/if} {#if !inline && !invalid && !warn && helperText} diff --git a/types/MultiSelect/MultiSelect.svelte.d.ts b/types/MultiSelect/MultiSelect.svelte.d.ts index 6018ad09a5..d218966c0d 100644 --- a/types/MultiSelect/MultiSelect.svelte.d.ts +++ b/types/MultiSelect/MultiSelect.svelte.d.ts @@ -27,15 +27,30 @@ export interface MultiSelectProps extends RestProps { itemToString?: (item: MultiSelectItem) => any; /** - * Override the item name, title, labelText passed to the checkbox input + * Override the item name, title, labelText, or value passed to the user-selectable checkbox input as well as the hidden inputs. * @default (item) => {} */ itemToInput?: (item: MultiSelectItem) => { name?: string; labelText?: any; title?: string; + value?: string; }; + /** + * Set to `true` to only render selected options as hidden inputs for form submission. + * @default false + */ + selectedOnly?: boolean; + + /** + * Combine selected items as comma-separated values when submitted in a form. + * If set to `true`, the default separator is a comma `,`. + * Pass in a string to override the separator. + * @default false + */ + combineValues?: false | true | string; + /** * Set the selected ids * @default []