Skip to content
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

Support html forms #121

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
22 changes: 21 additions & 1 deletion src/lib/components/listbox/Listbox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
horizontal?: boolean;
/** The selected value */
value?: StateDefinition["value"];
/** The name used when using this component inside a form. */
name?: string;
};
</script>

Expand All @@ -80,6 +82,8 @@
import type { HTMLActionArray } from "$lib/hooks/use-actions";
import Render from "$lib/utils/Render.svelte";
import type { TPassThroughProps } from "$lib/types";
import Hidden, { Features as HiddenFeatures } from "$lib/internal/Hidden.svelte";
import { objectToFormEntries } from "$lib/utils/form";

/***** Props *****/
type TAsProp = $$Generic<SupportedAs>;
Expand All @@ -90,6 +94,7 @@
export let disabled = false;
export let horizontal = false;
export let value: StateDefinition["value"];
export let name: string | null = null;

/***** Events *****/
const forwardEvents = forwardEventsBuilder(get_current_component(), [
Expand Down Expand Up @@ -276,6 +281,21 @@
</script>

<svelte:window on:mousedown={handleMousedown} />

{#if name != null && value != null}
{@const options = objectToFormEntries({ [name]: value })}
{#each options as [optionName, optionValue], index (index)}
<Hidden
features={HiddenFeatures.Hidden}
as="input"
type="hidden"
hidden
readonly
name={optionName}
value={optionValue}
/>
{/each}
{/if}
<Render
{...$$restProps}
{as}
Expand All @@ -284,4 +304,4 @@
name={"Listbox"}
>
<slot {...slotProps} />
</Render>
</Render>
154 changes: 154 additions & 0 deletions src/lib/components/listbox/listbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3692,3 +3692,157 @@ describe('Mouse interactions', () => {
})
)
})

describe('Form compatibility', () => {
it('should be possible to submit a form with a value', async () => {
let submitFn = jest.fn();

render(svelte`
<script>
let value = null
</script>

<form on:submit={(event) => {
event.preventDefault()
submitFn([...new FormData(event.currentTarget).entries()])
}}>
<Listbox value={value} on:change={(e) => (value = e.detail)} name="delivery">
<ListboxButton>Trigger</ListboxButton>
<ListboxOptions>
<ListboxOption value="pickup">Pickup</ListboxOption>
<ListboxOption value="home-delivery">Home delivery</ListboxOption>
<ListboxOption value="dine-in">Dine in</ListboxOption>
</ListboxOptions>
</Listbox>
<button type="submit">Submit</button>
</form>
`);

// Open listbox
await click(getListboxButton())

// Submit the form
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([]) // no data

// Open listbox again
await click(getListboxButton())

// Choose home delivery
await click(getByText('Home delivery'))

// Submit the form again
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([['delivery', 'home-delivery']])

// Open listbox again
await click(getListboxButton())

// Choose pickup
await click(getByText('Pickup'))

// Submit the form again
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([['delivery', 'pickup']])
})

it('should be possible to submit a form with a complex value object', async () => {
let submitFn = jest.fn();

render(svelte`
<script>
let options = [
{
id: 1,
value: 'pickup',
label: 'Pickup',
extra: { info: 'Some extra info' },
},
{
id: 2,
value: 'home-delivery',
label: 'Home delivery',
extra: { info: 'Some extra info' },
},
{
id: 3,
value: 'dine-in',
label: 'Dine in',
extra: { info: 'Some extra info' },
},
]

let value = options[0]
</script>

<form on:submit={(event) => {
event.preventDefault()
submitFn([...new FormData(event.currentTarget).entries()])
}}>
<Listbox value={value} on:change={(e) => (value = e.detail)} name="delivery">
<ListboxButton>Trigger</ListboxButton>
<ListboxOptions>
{#each options as option}
<ListboxOption value={option}>{option.label}</ListboxOption>
{/each}
</ListboxOptions>
</Listbox>
<button type="submit">Submit</button>
</form>
`);

// Open listbox
await click(getListboxButton())

// Submit the form
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([
['delivery[id]', '1'],
['delivery[value]', 'pickup'],
['delivery[label]', 'Pickup'],
['delivery[extra][info]', 'Some extra info'],
])

// Open listbox
await click(getListboxButton())

// Choose home delivery
await click(getByText('Home delivery'))

// Submit the form again
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([
['delivery[id]', '2'],
['delivery[value]', 'home-delivery'],
['delivery[label]', 'Home delivery'],
['delivery[extra][info]', 'Some extra info'],
])

// Open listbox
await click(getListboxButton())

// Choose pickup
await click(getByText('Pickup'))

// Submit the form again
await click(getByText('Submit'))

// Verify that the form has been submitted
expect(submitFn).lastCalledWith([
['delivery[id]', '1'],
['delivery[value]', 'pickup'],
['delivery[label]', 'Pickup'],
['delivery[extra][info]', 'Some extra info'],
])
})
})
20 changes: 20 additions & 0 deletions src/lib/components/radio-group/RadioGroup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
value: StateDefinition["value"];
/** Whether the `RadioGroup` and all of its `RadioGroupOption`s are disabled */
disabled?: boolean;
/** The name used when using this component inside a form. */
name?: string;
};
</script>

Expand All @@ -63,6 +65,8 @@
import type { HTMLActionArray } from "$lib/hooks/use-actions";
import Render from "$lib/utils/Render.svelte";
import type { TPassThroughProps } from "$lib/types";
import Hidden, { Features as HiddenFeatures } from "$lib/internal/Hidden.svelte";
import { objectToFormEntries } from "$lib/utils/form";

/***** Props *****/
type TAsProp = $$Generic<SupportedAs>;
Expand All @@ -72,6 +76,7 @@
export let use: HTMLActionArray = [];
export let value: StateDefinition["value"];
export let disabled = false;
export let name: string | null = null;

/***** Events *****/
const forwardEvents = forwardEventsBuilder(get_current_component(), [
Expand Down Expand Up @@ -223,6 +228,21 @@

<DescriptionProvider name="RadioGroupDescription" let:describedby>
<LabelProvider name="RadioGroupLabel" let:labelledby>
{#if name != null && value != null}
{@const options = objectToFormEntries({ [name]: value })}
{#each options as [optionName, optionValue], index (index)}
<Hidden
features={HiddenFeatures.Hidden}
as="input"
type="hidden"
hidden
readonly
name={optionName}
value={optionValue}
/>
{/each}
{/if}

<Render
{...{ ...$$restProps, ...propsWeControl }}
{as}
Expand Down
42 changes: 42 additions & 0 deletions src/lib/components/radio-group/radio-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,3 +771,45 @@ describe('Mouse interactions', () => {
expect(changeFn).toHaveBeenCalledTimes(1)
})
})

describe("`Enter`", () => {
it("should submit the form on `Enter`", async () => {
let submitFn = jest.fn();

render(svelte`
<script>
let selected = "home-delivery"
</script>

<form on:submit={(event) => {
event.preventDefault()
submitFn([...new FormData(event.currentTarget).entries()])
}}>
<RadioGroup value={selected} name="option" on:change={(e) => selected = e.detail}>
<RadioGroupLabel>Pizza Delivery</RadioGroupLabel>
<RadioGroupOption value="pickup">Pickup</RadioGroupOption>
<RadioGroupOption value="home-delivery">Home delivery</RadioGroupOption>
<RadioGroupOption value="dine-in">Dine in</RadioGroupOption>
</RadioGroup>
<button type="submit">Submit</button>
</form>
`);

// Submit the form
await click(getByText('Submit'))

// Verify the form was submitted with the home-delivery option
expect(submitFn).toHaveBeenCalledTimes(1);
expect(submitFn).toHaveBeenCalledWith([["option", "home-delivery"]]);

// Select the Pickup option
await click(getByText("Pickup"));

// Submit the form
await click(getByText('Submit'))

// Verify the form was submitted with the pickup option
expect(submitFn).toHaveBeenCalledTimes(2);
expect(submitFn).toHaveBeenCalledWith([["option", "pickup"]]);
});
});
19 changes: 19 additions & 0 deletions src/lib/components/switch/Switch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
> = TPassThroughProps<TSlotProps, TAsProp, "button"> & {
/** Whether the switch is checked */
checked: boolean;
/** The name used when using this component inside a form. */
name?: string;
/** The value used when using this component inside a form, if it is checked. */
value?: string;
};
</script>

Expand All @@ -22,6 +26,7 @@
import Render from "$lib/utils/Render.svelte";
import { resolveButtonType } from "$lib/utils/resolve-button-type";
import type { TPassThroughProps } from "$lib/types";
import Hidden, { Features as HiddenFeatures } from "$lib/internal/Hidden.svelte";

/***** Props *****/
type TAsProp = $$Generic<SupportedAs>;
Expand All @@ -30,6 +35,8 @@
export let as: SupportedAs = "button";
export let use: HTMLActionArray = [];
export let checked = false;
export let name: string | null = null;
export let value: string | null = null;

/***** Events *****/
const forwardEvents = forwardEventsBuilder(get_current_component(), [
Expand Down Expand Up @@ -81,6 +88,18 @@
$: slotProps = { checked };
</script>

{#if name != null && checked}
<Hidden
features={HiddenFeatures.Hidden}
as="input"
type="checkbox"
hidden
readonly
{name}
bind:checked
bind:value
/>
{/if}
<!-- TODO: I'm sure there's a better way of doing this -->
{#if switchStore}
<Render
Expand Down
Loading