This commit is contained in:
Cory LaViska
2024-12-02 13:33:31 -05:00
6 changed files with 189 additions and 12 deletions

View File

@@ -17,6 +17,7 @@
<li><a href="/frameworks/react">React</a></li>
<li><a href="/frameworks/vue">Vue</a></li>
<li><a href="/frameworks/angular">Angular</a></li>
<li><a href="/frameworks/svelte">Svelte</a></li>
</ul>
</li>
<li>

View File

@@ -1,4 +1,4 @@
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm';
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm';
(() => {
if (!window.scrollPositions) {
@@ -6,13 +6,13 @@ import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@7.3.0/+esm'
}
function preserveScroll() {
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
document.querySelectorAll('[data-preserve-scroll]').forEach(element => {
scrollPositions[element.id] = element.scrollTop;
});
}
function restoreScroll(event) {
document.querySelectorAll('[data-preserve-scroll').forEach(element => {
document.querySelectorAll('[data-preserve-scroll]').forEach(element => {
element.scrollTop = scrollPositions[element.id];
});

View File

@@ -0,0 +1,85 @@
---
meta:
title: Svelte
description: Tips for using Shoelace in your Svelte app.
---
# Svelte
Svelte [plays nice](https://custom-elements-everywhere.com/#svelte) with custom elements, so you can use Shoelace in your Svelte apps with ease.
## Installation
To add Shoelace to your Svelte app, install the package from npm.
```bash
npm install @shoelace-style/shoelace
```
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
```jsx
// main.js or main.ts
import '@shoelace-style/shoelace/dist/themes/light.css';
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/%CDNDIR%/');
```
:::tip
If you'd rather not use the CDN for assets, you can create a build task that copies `node_modules/@shoelace-style/shoelace/dist/assets` into a public folder in your app. Then you can point the base path to that folder instead.
:::
## Usage
### QR code generator example
```jsx
<h1>Live editing</h1>
<sl-input label="Message" value={message} oninput={event => message = event.target.value}></sl-input>
<sl-alert open>
<sl-icon slot="icon" name="info-circle"></sl-icon>
{message}
</sl-alert>
<script>
import '@shoelace-style/shoelace/dist/components/alert/alert.js'
import '@shoelace-style/shoelace/dist/components/input/input.js';
let message = $state('')
</script>
```
### Two-way Binding
One caveat is there's currently Svelte only supports `bind:value` directive in `<input>`, `<textarea>` and `<select>`, but you can still achieve two-way binding manually.
```jsx
// This doesn't work
<sl-input bind:value="name"></sl-input>
// This works, but it's a bit longer
<sl-input value={name} oninput={event => message = event.target.value}></sl-input>
```
:::tip
Are you using Shoelace with Svelte? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/svelte.md)
:::
### Slots
Slots in Shoelace/web components are functionally the same as basic slots in Svelte. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
Here is an example:
```jsx
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start" bind:open={drawerIsOpen}>
This drawer slides in from the start.
<div slot="footer">
<sl-button variant="primary" onclick={() => (drawerIsOpen = false)}>
Close
</sl-button>
</div>
</sl-drawer>
```

View File

@@ -18,6 +18,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Added Ukrainian translation [#2270]
- Added support for <kbd>Enter</kbd> to `<sl-split-panel>` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) [#2234]
- Added community maintained docs for Svelte [#2262]
- Fixed a bug in `<sl-select>` when setting the value property before the element connected. [#2255]
- Fixed a bug in `<sl-select>` where it was using the wrong tag name. [#2287]
- Fixed a bug in `<sl-carousel>` that caused the navigation icons to be reversed
- Fixed a bug in `<sl-carousel>` that caused interactive elements to be activated when dragging [#2196]
- Fixed a bug in `<sl-carousel>` that caused out of order slides when used inside a resize observer [#2260]

View File

@@ -1,6 +1,5 @@
import { animateTo, stopAnimations } from '../../internal/animate.js';
import { classMap } from 'lit/directives/class-map.js';
import { defaultValue } from '../../internal/default-value.js';
import { FormControlController } from '../../internal/form.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { HasSlotController } from '../../internal/slot.js';
@@ -102,21 +101,35 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
/** The name of the select, submitted as a name/value pair with form data. */
@property() name = '';
private _value: string | string[] = '';
get value() {
return this._value;
}
/**
* The current value of the select, submitted as a name/value pair with form data. When `multiple` is enabled, the
* value attribute will be a space-delimited list of values based on the options selected, and the value property will
* be an array. **For this reason, values must not contain spaces.**
*/
@property({
converter: {
fromAttribute: (value: string) => value.split(' '),
toAttribute: (value: string[]) => value.join(' ')
@state()
set value(val: string | string[]) {
if (this.multiple) {
val = Array.isArray(val) ? val : val.split(' ');
} else {
val = Array.isArray(val) ? val.join(' ') : val;
}
})
value: string | string[] = '';
if (this._value === val) {
return;
}
this.valueHasChanged = true;
this._value = val;
}
/** The default value of the form control. Primarily used for resetting the form control. */
@defaultValue() defaultValue: string | string[] = '';
@property({ attribute: 'value' }) defaultValue: string | string[] = '';
/** The select's size. */
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
@@ -451,6 +464,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
private handleClearClick(event: MouseEvent) {
event.stopPropagation();
this.valueHasChanged = true;
if (this.value !== '') {
this.setSelectedOptions([]);
this.displayInput.focus({ preventScroll: true });
@@ -522,6 +537,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
private handleTagRemove(event: SlRemoveEvent, option: SlOption) {
event.stopPropagation();
this.valueHasChanged = true;
if (!this.disabled) {
this.toggleOptionSelection(option, false);
@@ -598,6 +615,9 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
// Update selected options cache
this.selectedOptions = options.filter(el => el.selected);
// Keep a reference to the previous `valueHasChanged`. Changes made here don't count has changing the value.
const cachedValueHasChanged = this.valueHasChanged;
// Update the value and display label
if (this.multiple) {
this.value = this.selectedOptions.map(el => el.value);
@@ -613,12 +633,14 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
this.value = selectedOption?.value ?? '';
this.displayLabel = selectedOption?.getTextLabel?.() ?? '';
}
this.valueHasChanged = cachedValueHasChanged;
// Update validity
this.updateComplete.then(() => {
this.formControlController.updateValidity();
});
}
protected get tags() {
return this.selectedOptions.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
@@ -649,8 +671,29 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
}
@watch('value', { waitUntilFirstUpdate: true })
attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null) {
super.attributeChangedCallback(name, oldVal, newVal);
/** This is a backwards compatibility call. In a new major version we should make a clean separation between "value" the attribute mapping to "defaultValue" property and "value" the property not reflecting. */
if (name === 'value') {
const cachedValueHasChanged = this.valueHasChanged;
this.value = this.defaultValue;
// Set it back to false since this isn't an interaction.
this.valueHasChanged = cachedValueHasChanged;
}
}
@watch(['defaultValue', 'value'], { waitUntilFirstUpdate: true })
handleValueChange() {
if (!this.valueHasChanged) {
const cachedValueHasChanged = this.valueHasChanged;
this.value = this.defaultValue;
// Set it back to false since this isn't an interaction.
this.valueHasChanged = cachedValueHasChanged;
}
const allOptions = this.getAllOptions();
const value = Array.isArray(this.value) ? this.value : [this.value];

View File

@@ -740,6 +740,52 @@ describe('<sl-select>', () => {
expect(new FormData(form).getAll('select')).to.have.members(['foo', 'bar', 'baz']);
});
});
/**
* @see {https://github.com/shoelace-style/shoelace/issues/2254}
*/
it('Should account for if `value` changed before connecting', async () => {
const select = await fixture<SlSelect>(html`
<sl-select label="Search By" multiple clearable .value=${['foo', 'bar']}>
<sl-option value="foo">Foo</sl-option>
<sl-option value="bar">Bar</sl-option>
</sl-select>
`);
// just for safe measure.
await aTimeout(10);
expect(select.value).to.deep.equal(['foo', 'bar']);
});
/**
* @see {https://github.com/shoelace-style/shoelace/issues/2254}
*/
it('Should still work if using the value attribute', async () => {
const select = await fixture<SlSelect>(html`
<sl-select label="Search By" multiple clearable value="foo bar">
<sl-option value="foo">Foo</sl-option>
<sl-option value="bar">Bar</sl-option>
</sl-select>
`);
// just for safe measure.
await aTimeout(10);
expect(select.value).to.deep.equal(['foo', 'bar']);
await clickOnElement(select);
await select.updateComplete;
await clickOnElement(select.querySelector("[value='foo']")!);
await select.updateComplete;
await aTimeout(10);
expect(select.value).to.deep.equal(['bar']);
select.setAttribute('value', 'foo bar');
await aTimeout(10);
expect(select.value).to.deep.equal(['foo', 'bar']);
});
});
runFormControlBaseTests('sl-select');