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

feat: controlProps() and labelProps() #206

Open
wants to merge 4 commits into
base: main
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
5 changes: 5 additions & 0 deletions .changeset/fresh-coats-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"formsnap": minor
---

feat: add `controlProps()` and `labelProps()` to simplify usage
37 changes: 26 additions & 11 deletions docs/src/content/components/control.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,27 @@ The `Control` component doesn't render an element itself, it strictly provides c
## Usage

```svelte title="+page.svelte"
<script lang="ts">
import { Control, controlProps } from "formsnap";
</script>

<Control>
<input type="text" {...controlProps()} bind:value={$formData.name} />
</Control>
```

## controlProps

The `controlProps` function returns the props for the closest ancestor `Control` component. Use this function to spread the props onto the control element/component.

```svelte
<script lang="ts">
import { Control, controlProps } from "formsnap";
</script>

<Control>
{#snippet children({ props })}
<input type="text" {...props} bind:value={$formData.name} />
{/snippet}
<!-- controlProps() gets the props from the closest <Control> -->
<input type="text" {...controlProps()} bind:value={$formData.name} />
</Control>
```

Expand Down Expand Up @@ -60,7 +77,7 @@ Here's how you might do just that:

```svelte title="CustomControl.svelte"
<script lang="ts">
import { Control, Label } from "formsnap";
import { Control, Label, controlProps } from "formsnap";
import type { ComponentProps } from "svelte";

let {
Expand All @@ -75,12 +92,10 @@ Here's how you might do just that:
</script>

<Control {...restProps}>
{#snippet children({ props })}
<div class="flex flex-col gap-2">
<Label>{label}</Label>
<!-- Forward the props to the children snippet -->
{@render childrenProp({ props })}
</div>
{/snippet}
<div class="flex flex-col gap-2">
<Label>{label}</Label>
<!-- Forward the props to the children snippet -->
{@render childrenProp({ props: controlProps() })}
</div>
</Control>
```
8 changes: 3 additions & 5 deletions docs/src/content/components/description.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ Descriptions must be used within the context of a [Field](/docs/components/field

## Usage

```svelte {8}
```svelte {6}
<Field name="name" {form}>
<Control>
{#snippet children({ props })}
<Label>Name</Label>
<input type="text" {...attrs} />
{/snippet}
<Label>Name</Label>
<input type="text" {...controlProps()} />
</Control>
<Description>Your full name, including your middle name.</Description>
</Field>
Expand Down
4 changes: 1 addition & 3 deletions docs/src/content/components/element-field.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ Here's an example of how you might use the `ElementField` component to create a
{#each $formData.urls as _, i}
<ElementField {form} name="urls[{i}]">
<Control>
{#snippet children({ props })}
<input type="url" bind:value={$formData.urls[i]} {...props} />
{/snippet}
<input type="url" bind:value={$formData.urls[i]} {...controlProps()} />
</Control>
<FieldErrors />
</ElementField>
Expand Down
26 changes: 11 additions & 15 deletions docs/src/content/components/fieldset.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@ This component automatically includes the [Field](/docs/components/field) compon

When you have a group of radio buttons related to a single field, you should use a `Fieldset` to group them together.

```svelte {1-2,13}
```svelte {1-2,11}
<Fieldset {form} name="theme">
<Legend>Select your theme</Legend>
{#each themes as theme}
<Control>
{#snippet children({ props })}
<input {...props} type="radio" bind:group={$formData.theme} value={theme} />
<Label>{theme}</Label>
{/snippet}
<input {...controlProps()} type="radio" bind:group={$formData.theme} value={theme} />
<Label>{theme}</Label>
</Control>
{/each}
<Description>Help us understand your preferences by selecting a theme.</Description>
Expand All @@ -38,20 +36,18 @@ When you have a group of radio buttons related to a single field, you should use

When you have a group of checkboxes related to a single field, typically used for multiple selections, you should use a `Fieldset` to group them together.

```svelte {1-2,18}
```svelte {1-2,16}
<Fieldset {form} name="allergies">
<Legend>Any food allergies?</Legend>
{#each allergies as allergy}
<Control>
{#snippet children({ props })}
<input
{...props}
type="checkbox"
bind:group={$formData.allergies}
value={allergy}
/>
<Label>{allergy}</Label>
{/snippet}
<input
{...controlProps()}
type="checkbox"
bind:group={$formData.allergies}
value={allergy}
/>
<Label>{allergy}</Label>
</Control>
{/each}
<Description>We'll make sure to accommodate your dietary needs.</Description>
Expand Down
6 changes: 2 additions & 4 deletions docs/src/content/components/label.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ When using a `Label` inside a [Control](/docs/components/control), you don't nee
```svelte {3}
<Field {form} name="name">
<Control>
{#snippet children({ props })}
<Label>Name</Label>
<input type="text" {...props} />
{/snippet}
<Label>Name</Label>
<input type="text" {...controlProps()} />
</Control>
</Field>
```
Expand Down
20 changes: 20 additions & 0 deletions docs/src/content/composition/control-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: controlProps
description: Retrieves the props from the closest Control component's context.
section: Composition
---

Use the `controlProps` function to retrieve a reactive reference to the props from the closest `Control` component's context.

## Usage

```svelte
<script lang="ts">
import { Label, Control, controlProps } from "formsnap";
</script>

<Control>
<Label>Hello</Label>
<input type="text" {...controlProps()} bind:value={$formData.name} />
</Control>
```
20 changes: 20 additions & 0 deletions docs/src/content/composition/label-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: labelProps
description: Retrieves the label props from the closest Control component's context.
section: Composition
---

Use the `labelProps` function to retrieve a reactive reference to the label props from the closest `Control` component's context. The label props are used to associate the control element with the label.

## Usage

```svelte
<script lang="ts">
import { Control, controlProps, labelProps } from "formsnap";
</script>

<Control>
<label {...labelProps()}>Hello</label>
<input type="text" {...controlProps()} bind:value={$formData.name} />
</Control>
```
20 changes: 7 additions & 13 deletions docs/src/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ All is not lost though, as the whole idea behind Formsnap is to make this proces

```svelte title="+page.svelte"
<script lang="ts">
import { Field, Control, Label, FieldErrors, Description } from "formsnap";
import { Field, Control, Label, FieldErrors, Description, controlProps } from "formsnap";
import { signupFormSchema } from "./schema.ts";
import { zodClient } from "sveltekit-superforms/adapters";
import { superForm } from "sveltekit-superforms";
Expand All @@ -106,30 +106,24 @@ All is not lost though, as the whole idea behind Formsnap is to make this proces
<form method="POST" use:enhance>
<Field {form} name="name">
<Control>
{#snippet children({ props })}
<Label>Name</Label>
<input {...props} bind:value={$formData.name} />
{/snippet}
<Label>Name</Label>
<input {...controlProps()} bind:value={$formData.name} />
</Control>
<Description>Be sure to use your real name.</Description>
<FieldErrors />
</Field>
<Field {form} name="email">
<Control>
{#snippet children({ props })}
<Label>Email</Label>
<input {...props} type="email" bind:value={$formData.email} />
{/snippet}
<Label>Email</Label>
<input {...controlProps()} type="email" bind:value={$formData.email} />
</Control>
<Description>It's preferred that you use your company email.</Description>
<FieldErrors />
</Field>
<Field {form} name="password">
<Control>
{#snippet children({ props })}
<Label>Password</Label>
<input {...props} type="password" bind:value={$formData.password} />
{/snippet}
<Label>Password</Label>
<input {...controlProps()} type="password" bind:value={$formData.password} />
</Control>
<Description>Ensure the password is at least 10 characters.</Description>
<FieldErrors />
Expand Down
80 changes: 40 additions & 40 deletions docs/src/content/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Now let's add the remaining parts of the field:
```svelte title="src/routes/settings/+page.svelte"
<script lang="ts">
import { superForm } from "sveltekit-superforms";
import { Field, Control, Label, Description, FieldErrors } from "formsnap";
import { Field, Control, Label, Description, FieldErrors, controlProps } from "formsnap";
import { zodClient } from "sveltekit-superforms/adapters";
import { allergies, schema, themes } from "./schema.js";
import SuperDebug from "sveltekit-superforms";
Expand All @@ -152,10 +152,8 @@ Now let's add the remaining parts of the field:
<form method="POST" use:enhance>
<Field {form} name="email">
<Control>
{#snippet children({ props })}
<Label>Email</Label>
<input {...props} type="email" bind:value={$formData.email} />
{/snippet}
<Label>Email</Label>
<input {...controlProps()} type="email" bind:value={$formData.email} />
</Control>
<Description>Use your company email if you have one.</Description>
<FieldErrors />
Expand All @@ -164,7 +162,7 @@ Now let's add the remaining parts of the field:
<SuperDebug data={$formData} />
```

We've first added the [Control](/docs/components/control) component. `Control`s are used to represent a form control and its label. They keep the control and label in sync via the `props` snippet prop, which is spread onto the control. Inside the `Control`, we've added the [Label](/docs/components/label) component, which will automatically associate itself with the control the `props` are spread onto. We've also added the control itself, which is an `input` that we're binding to the `email` property of the form data.
We've first added the [Control](/docs/components/control) component. `Control`s are used to represent a form control and its label. They keep the control and label in sync via the `controlProps()` function, which is provided by the `Control` component and is spread onto the control element. Inside the `Control`, we've added the [Label](/docs/components/label) component, which will automatically associate itself with the control the `props` are spread onto. We've also added the control itself, which is an `input` that we're binding to the `email` property of the form data.

The [Description](/docs/components/description) component is optional, but it's useful for providing additional context to the user about the field. It'll be synced with the `aria-describedby` attribute on the input, so it's accessible to screen readers.

Expand All @@ -177,7 +175,16 @@ And that's really all it takes to setup a form field. Let's continue on with the
```svelte title="src/routes/settings/+page.svelte"
<script lang="ts">
import { superForm } from "sveltekit-superforms";
import { Field, Control, Label, Description, FieldErrors, Fieldset, Legend } from "formsnap";
import {
Field,
Control,
Label,
Description,
FieldErrors,
Fieldset,
Legend,
controlProps,
} from "formsnap";
import { zodClient } from "sveltekit-superforms/adapters";
import { allergies, schema, themes } from "./schema.js";
import SuperDebug from "sveltekit-superforms";
Expand All @@ -193,34 +200,28 @@ And that's really all it takes to setup a form field. Let's continue on with the
<form use:enhance class="mx-auto flex max-w-md flex-col" method="POST">
<Field {form} name="email">
<Control>
{#snippet children({ props })}
<Label>Email</Label>
<input {...props} type="email" bind:value={$formData.email} />
{/snippet}
<Label>Email</Label>
<input {...controlProps()} type="email" bind:value={$formData.email} />
</Control>
<Description>Company email is preferred</Description>
<FieldErrors />
</Field>
<Field {form} name="bio">
<Control>
{#snippet children({ props })}
<Label>Bio</Label>
<textarea {...props} bind:value={$formData.bio} />
{/snippet}
<Label>Bio</Label>
<textarea {...controlProps()} bind:value={$formData.bio} />
</Control>
<Description>Tell us a bit about yourself.</Description>
<FieldErrors />
</Field>
<Field {form} name="language">
<Control>
{#snippet children({ props })}
<Label>Language</Label>
<select {...props} bind:value={$formData.language}>
<option value="fr">French</option>
<option value="es">Spanish</option>
<option value="en">English</option>
</select>
{/snippet}
<Label>Language</Label>
<select {...controlProps()} bind:value={$formData.language}>
<option value="fr">French</option>
<option value="es">Spanish</option>
<option value="en">English</option>
</select>
</Control>
<Description>Help us address you properly.</Description>
<FieldErrors />
Expand All @@ -229,21 +230,22 @@ And that's really all it takes to setup a form field. Let's continue on with the
<Legend>Select your theme</Legend>
{#each themes as theme}
<Control>
{#snippet children({ props })}
<Label>{theme}</Label>
<input {...props} type="radio" value={theme} bind:group={$formData.theme} />
{/snippet}
<Label>{theme}</Label>
<input
{...controlProps()}
type="radio"
value={theme}
bind:group={$formData.theme}
/>
</Control>
{/each}
<Description>We prefer dark mode, but the choice is yours.</Description>
<FieldErrors />
</Fieldset>
<Field {form} name="marketingEmails">
<Control>
{#snippet children({ props })}
<input {...props} type="checkbox" bind:checked={$formData.marketingEmails} />
<Label>I want to receive marketing emails</Label>
{/snippet}
<input {...controlProps()} type="checkbox" bind:checked={$formData.marketingEmails} />
<Label>I want to receive marketing emails</Label>
</Control>
<Description>Stay up to date with our latest news and offers.</Description>
<FieldErrors />
Expand All @@ -252,15 +254,13 @@ And that's really all it takes to setup a form field. Let's continue on with the
<Legend>Food allergies</Legend>
{#each allergies as allergy}
<Control>
{#snippet children({ props })}
<input
{...props}
type="checkbox"
bind:group={$formData.allergies}
value={allergy}
/>
<Label>{allergy}</Label>
{/snippet}
<input
{...controlProps()}
type="checkbox"
bind:group={$formData.allergies}
value={allergy}
/>
<Label>{allergy}</Label>
</Control>
{/each}
<Description>When we provide lunch, we'll accommodate your needs.</Description>
Expand Down
Loading