Skip to content

Rework setting initial values in <wa-select> #1063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 74 additions & 71 deletions packages/webawesome/docs/docs/components/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,47 +109,51 @@ Use the `disabled` attribute to disable a select.

### Multiple

To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `with-clear` when this option is enabled. To set multiple values at once, set `value` to a space-delimited list of values.
To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `with-clear` when this option is enabled. You can select multiple options by adding the `selected` attribute to individual options.

```html {.example}
<wa-select label="Select a Few" value="option-1 option-2 option-3" multiple with-clear>
<wa-option value="option-1">Option 1</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
<wa-select label="Select a Few" multiple with-clear>
<wa-option value="option-1" selected>Option 1</wa-option>
<wa-option value="option-2" selected>Option 2</wa-option>
<wa-option value="option-3" selected>Option 3</wa-option>
<wa-option value="option-4">Option 4</wa-option>
<wa-option value="option-5">Option 5</wa-option>
<wa-option value="option-6">Option 6</wa-option>
</wa-select>
```

:::info
Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
Selecting multiple options may result in wrapping, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
:::

### Setting Initial Values

Use the `value` attribute to set the initial selection.
Use the `selected` attribute on individual options to set the initial selection, similar to native HTML.

```html {.example}
<wa-select value="option-1">
<wa-option value="option-1">Option 1</wa-option>
<wa-select>
<wa-option value="option-1" selected>Option 1</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
<wa-option value="option-4">Option 4</wa-option>
</wa-select>
```

When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<wa-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
For multiple selections, apply it to all selected options.

```html {.example}
<wa-select value="option-1 option-2" multiple with-clear>
<wa-option value="option-1">Option 1</wa-option>
<wa-option value="option-2">Option 2</wa-option>
<wa-select multiple with-clear>
<wa-option value="option-1" selected>Option 1</wa-option>
<wa-option value="option-2" selected>Option 2</wa-option>
<wa-option value="option-3">Option 3</wa-option>
<wa-option value="option-4">Option 4</wa-option>
</wa-select>
```

:::info
Framework users can bind directly to the `value` property for reactive data binding and form state management.
:::

### Grouping Options

Use `<wa-divider>` to group listbox items visually. You can also use `<small>` to provide labels, but they won't be announced by most assistive devices.
Expand Down Expand Up @@ -240,23 +244,17 @@ Use the `start` and `end` slots to add presentational elements like `<wa-icon>`

### Custom Tags

When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<wa-option>` element and the second argument is the tag's index (its position in the tag list).
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a [Lit Template](https://lit.dev/docs/templates/overview/), or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<wa-option>` element and the second argument is the tag's index (its position in the tag list).

Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/docs/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.

```html {.example}
<wa-select
placeholder="Select one"
value="email phone"
multiple
with-clear
class="custom-tag"
>
<wa-option value="email">
<wa-select placeholder="Select one" multiple with-clear class="custom-tag">
<wa-option value="email" selected>
<wa-icon slot="start" name="envelope" variant="solid"></wa-icon>
Email
</wa-option>
<wa-option value="phone">
<wa-option value="phone" selected>
<wa-icon slot="start" name="phone" variant="solid"></wa-icon>
Phone
</wa-option>
Expand All @@ -267,9 +265,9 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
</wa-select>

<script type="module">
await customElements.whenDefined("wa-select")
await customElements.whenDefined('wa-select');
const select = document.querySelector('.custom-tag');
await select.updateComplete
await select.updateComplete;

select.getTag = (option, index) => {
// Use the same icon used in wa-option
Expand All @@ -292,17 +290,15 @@ Be sure you trust the content you are outputting! Passing unsanitized user input

### Lazy loading options

Lazy loading options is very hard to get right. `<wa-select>` largely follows how a native `<select>` works.

Here are the following conditions:
Lazy loading options works similarly to native `<select>` elements. The select component handles various scenarios intelligently:

- If a `<wa-select>` is created without any options, but is given a `value` attribute, its `value` will be `""`, and then when options are added, if any of the options have a value equal to the `<wa-select>` value, the value of the `<wa-select>` will equal that of the option.
#### Basic lazy loading scenarios:

EX: `<wa-select value="foo">` will have a value of `""` until `<wa-option value="foo">Foo</wa-option>` connects, at which point its value will become `"foo"` when submitting.
- **Empty select with value**: If a `<wa-select>` is created without any options but given a `value` attribute, its value will be `""` initially. When options are added later, if any option has a value matching the select's value attribute, the select's value will update to match.

- If a `<wa-select multiple>` with an initial value has multiple values, but only some of the options are present, it will only respect the options that are present, and if a selected option is loaded in later, *AND* the value of the select has not changed via user interaction or direct property assignment, it will add the selected option to the form value and to the `.value` of the select.
- **Multiple select with partial options**: If a `<wa-select multiple>` has an initial value with multiple options, but only some options are present in the DOM, it will respect only the available options. When additional selected options are loaded later (and the user hasn't changed the selection), those options will be automatically added to the selection.

This can be hard to conceptualize, so heres a fairly large example showing how lazy loaded options work with `<wa-select>` and `<wa-select multiple>` when given initial value attributes. Feel free to play around with it in a codepen.
Here's a comprehensive example showing different lazy loading scenarios:

```html {.example}
<form id="lazy-options-example">
Expand All @@ -311,93 +307,100 @@ This can be hard to conceptualize, so heres a fairly large example showing how l
<wa-option value="bar">Bar</wa-option>
<wa-option value="baz">Baz</wa-option>
</wa-select>
<br>
<br />
<wa-button type="button">Add "foo" option</wa-button>
</div>

<br>
<br />

<div>
<wa-select name="select-2" value="foo" label="Single select (with no existing options)">
</wa-select>
<br>
<wa-select name="select-2" value="foo" label="Single select (with no existing options)"> </wa-select>
<br />
<wa-button type="button">Add "foo" option</wa-button>
</div>

<br>
<br />

<div>
<wa-select name="select-3" value="foo bar baz" multiple label="Multiple Select (with existing options)">
<wa-option value="bar">Bar</wa-option>
<wa-option value="baz">Baz</wa-option>
<wa-select name="select-3" multiple label="Multiple Select (with existing selected options)">
<wa-option value="bar" selected>Bar</wa-option>
<wa-option value="baz" selected>Baz</wa-option>
</wa-select>
<br>
<wa-button type="button">Add "foo" option</wa-button>
<br />
<wa-button type="button">Add "foo" option (selected)</wa-button>
</div>

<br>
<br />

<div>
<wa-select name="select-4" value="foo" multiple label="Multiple Select (with no existing options)">
</wa-select>
<br>
<wa-select name="select-4" value="foo" multiple label="Multiple Select (with no existing options)"> </wa-select>
<br />
<wa-button type="button">Add "foo" option</wa-button>
</div>

<br><br>
<br /><br />

<div style="display: flex; gap: 16px;">
<wa-button type="reset">Reset</wa-button>
<wa-button type="submit" variant="brand">Show FormData</wa-button>
</div>

<br>
<br />

<pre hidden><code id="lazy-options-example-form-data"></code></pre>

<br>
<br />
</form>

<script type="module">
function addFooOption(e) {
const addFooButton = e.target.closest("wa-button[type='button']")
const addFooButton = e.target.closest("wa-button[type='button']");
if (!addFooButton) {
return
return;
}
const select = addFooButton.parentElement.querySelector("wa-select")
const select = addFooButton.parentElement.querySelector('wa-select');

if (select.querySelector("wa-option[value='foo']")) {
// Foo already exists. no-op.
return
return;
}

const option = document.createElement("wa-option")
option.setAttribute("value", "foo")
option.innerText = "Foo"
select.append(option)
const option = document.createElement('wa-option');
option.setAttribute('value', 'foo');
option.innerText = 'Foo';

// For the multiple select with existing selected options, make the new option selected
if (select.getAttribute('name') === 'select-3') {
option.selected = true;
}

select.append(option);
}

function handleLazySubmit (event) {
event.preventDefault()
function handleLazySubmit(event) {
event.preventDefault();

const formData = new FormData(event.target)
const codeElement = document.querySelector("#lazy-options-example-form-data")
const formData = new FormData(event.target);
const codeElement = document.querySelector('#lazy-options-example-form-data');

const obj = {}
const obj = {};
for (const key of formData.keys()) {
const val = formData.getAll(key).length > 1 ? formData.getAll(key) : formData.get(key)
obj[key] = val
const val = formData.getAll(key).length > 1 ? formData.getAll(key) : formData.get(key);
obj[key] = val;
}

codeElement.textContent = JSON.stringify(obj, null, 2)
codeElement.textContent = JSON.stringify(obj, null, 2);

const preElement = codeElement.parentElement
preElement.removeAttribute("hidden")
const preElement = codeElement.parentElement;
preElement.removeAttribute('hidden');
}

const container = document.querySelector("#lazy-options-example")
container.addEventListener("click", addFooOption)
container.addEventListener("submit", handleLazySubmit)
const container = document.querySelector('#lazy-options-example');
container.addEventListener('click', addFooOption);
container.addEventListener('submit', handleLazySubmit);
</script>
```

:::info
The key principle is that the select component prioritizes user interactions and explicit selections over programmatic changes, ensuring a predictable user experience even with dynamically loaded content.
:::
3 changes: 2 additions & 1 deletion packages/webawesome/docs/docs/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ During the alpha period, things might break! We take breaking changes very serio
- `<wa-input>`
- `<wa-select>`
- `<wa-option>`
- 🚨 BREAKING: reworked `<wa-select>` to use `<wa-option selected>` to set initially selected options, removing the "no spaces allowed" restrictions for option values
- Added a new free component: `<wa-popover>` (#2 of 14 per stretch goals)
- Added a new free component: `<wa-zoomable-frame>` (#3 of 14 per stretch goals)
- Added a `min-block-size` to `<wa-divider orientation="vertical">` to ensure the divider is visible regardless of container height [issue:675]
Expand Down Expand Up @@ -378,4 +379,4 @@ Here's a list of some of the things that have changed since Shoelace v2. For que

Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)

Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)
Are you coming from Shoelace? [The 2.x changelog can be found here.](https://shoelace.style/resources/changelog/)
33 changes: 20 additions & 13 deletions packages/webawesome/src/components/option/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ export default class WaOption extends WebAwesomeElement {
// Set via the parent select
@state() current = false;

@state() selected = false;

/**
* The option's value. When selected, the containing form control will receive this value. The value must be unique
* from other options in the same group. Values may not contain spaces, as spaces are used as delimiters when listing
Expand All @@ -56,7 +54,13 @@ export default class WaOption extends WebAwesomeElement {
@property({ reflect: true }) value = '';

/** Draws the option in a disabled state, preventing selection. */
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) disabled = false;

/** @internal */
@property({ type: Boolean, attribute: false }) selected = false;

/** Selects an option initially. */
@property({ type: Boolean, attribute: 'selected' }) defaultSelected = false;

_label: string = '';
/**
Expand Down Expand Up @@ -107,10 +111,6 @@ export default class WaOption extends WebAwesomeElement {

private handleDefaultSlotChange() {
// Tell the controller to update the label
if (customElements.get('wa-select')) {
this.closest('wa-select')?.selectionChanged();
}

this.updateDefaultLabel();

if (this.isInitialized) {
Expand All @@ -119,6 +119,7 @@ export default class WaOption extends WebAwesomeElement {
const controller = this.closest('wa-select');
if (controller) {
controller.handleDefaultSlotChange();
controller.selectionChanged?.();
}
});
} else {
Expand All @@ -136,6 +137,17 @@ export default class WaOption extends WebAwesomeElement {
}
};

protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has('defaultSelected')) {
if (!this.closest('wa-select')?.hasInteracted) {
const oldVal = this.selected;
this.selected = this.defaultSelected;
this.requestUpdate('selected', oldVal);
}
}
super.willUpdate(changedProperties);
}

updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);

Expand All @@ -146,6 +158,7 @@ export default class WaOption extends WebAwesomeElement {
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
this.customStates.set('selected', this.selected);
this.handleDefaultSlotChange();
}

if (changedProperties.has('value')) {
Expand All @@ -155,12 +168,6 @@ export default class WaOption extends WebAwesomeElement {
this.value = String(this.value);
}

if (this.value.includes(' ')) {
// eslint-disable-next-line no-console
console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this);
this.value = this.value.replace(/ /g, '_');
}

this.handleDefaultSlotChange();
}

Expand Down
Loading