Skip to main content

Dynamic Fields

Learn how to creating dynamic fields by building a URLs field with Formsnap.

To create a dynamic field, you'll need to use the ElementField component, that allows you to treat each element of an array as it's own field.

In this recipe, we'll create a URLs field where users can add and remove URLs from their profile.

Create Dynamic Fields

Define the Schema

Here's the schema we'll use for the form we'll build in this guide. We'll assume you know how to setup the load function and actions in the +page.server.ts file.

schema.ts
		import { z } from "zod";
 
export const schema = z.object({
	urls: z
		.array(z.string().url({ message: "Please enter a valid URL." }))
		.min(2, "You must include at least two URLs on your profile.")
		.default(["", ""]),
});
	

We've defined an array named urls that contains strings that must be valid URLs. We've also set a minimum length of 2 for the array itself, and provided two default values to start with. The minimum length of 2 may sounds strange, but we're only doing so to demonstrate different validation errors for the array and its elements.

Create the Form

We'll need to initialize our SuperForm with the form returned from the load function, and then setup the basic structure of our form.

+page.svelte
		<script lang="ts">
	import { superForm } from "sveltekit-superforms";
	import { zodClient } from "sveltekit-superforms/adapters";
	import { schema } from "./schema.js";
 
	let { data } = $props();
 
	const form = superForm(data.form, {
		validators: zodClient(schema),
	});
 
	const { form: formData } = form;
</script>
 
<form use:form.enhance method="POST">
	<!-- ... -->
	<button type="submit">Submit</button>
</form>
	

Import the Components

We have a few components we need to import to build the form.

+page.svelte
		<script lang="ts">
	import { superForm } from "sveltekit-superforms";
	import { zodClient } from "sveltekit-superforms/adapters";
	import {
		Fieldset,
		Legend,
		ElementField,
		Control,
		Label,
		FieldErrors,
		Description,
	} from "formsnap";
	import { schema } from "./schema.js";
 
	let { data } = $props();
 
	const form = superForm(data.form, {
		validators: zodClient(schema),
	});
 
	const { form: formData } = form;
</script>
 
<form use:form.enhance method="POST">
	<!-- ... -->
	<button type="submit">Submit</button>
</form>
	

Scaffold the Fieldset

Since our individual URL inputs will be part of the same field, we'll use a Fieldset component to group them together and a Legend to provide a title.

+page.svelte
		<!-- script tag -->
<form use:form.enhance method="POST">
	<Fieldset {form} name="urls">
		<Legend>Public URLs</Legend>
		<!-- ... -->
		<Description>Add URLs to your profile that you'd like to share with others.</Description>
		<FieldErrors />
	</Fieldset>
	<button type="submit">Submit</button>
</form>
	

The FieldErrors component will display any validation errors for the array itself. In our case, it will display an error if the array doesn't contain at least two URLs (we'll add the errors for the individual URLs in the next step).

The Description component will provide additional context about the fields once we've created them, but each field will share the same description from the Fieldset scope.

Render the URL Fields

Now that we've scaffolded the Fieldset, we can iterate over the $formData.urls array to render the individual URL fields, which are represented by the ElementField component.

+page.svelte
		<!-- script tag -->
<form use:enhance method="POST">
	<Fieldset {form} name="urls">
		<Legend>Public URLs</Legend>
		{#each $formData.urls as _, i}
			<ElementField {form} name="urls[{i}]">
				<Control>
					{#snippet children({ props })}
						<Label class="sr-only">URL {i + 1}</Label>
						<input type="url" {...props} bind:value={$formData.urls[i]} />
					{/snippet}
				</Control>
				<Description class="sr-only">
					This URL will be publicly available on your profile.
				</Description>
				<FieldErrors />
			</ElementField>
		{/each}
		<FieldErrors />
	</Fieldset>
	<button type="submit">Submit</button>
</form>
	

We're using the ElementField component to treat each element of the urls array as a separate field with its own state and validation. We're also using the Control component to create a label and input for each URL, and binding the input's value to the corresponding element of the urls array.

Make the Fields Dynamic

At the moment, the user can only have two URLs in their profile. We want to allow them to add and remove URLs as needed. We can achieve this by adding buttons to add and remove URLs.

+page.svelte
		<script lang="ts">
	import { superForm } from "sveltekit-superforms";
	import { zodClient } from "sveltekit-superforms/adapters";
	import {
		Fieldset,
		Legend,
		ElementField,
		Control,
		Label,
		FieldErrors,
		Description,
	} from "formsnap";
	import { schema } from "./schema.js";
 
	let { data } = $props();
 
	const form = superForm(data.form, {
		validators: zodClient(schema),
	});
 
	const { form: formData } = form;
 
	function removeUrlByIndex(index: number) {
		$formData.urls = $formData.urls.filter((_, i) => i !== index);
	}
 
	function addUrl() {
		$formData.urls = [...$formData.urls, ""];
	}
</script>
 
<form use:form.enhance method="POST">
	<Fieldset {form} name="urls">
		<Legend>Public URLs</Legend>
		{#each $formData.urls as _, i}
			<ElementField {form} name="urls[{i}]">
				<Control>
					{#snippet children({ props })}
						<Label class="sr-only">URL {i + 1}</Label>
						<input type="url" {...props} bind:value={$formData.urls[i]} />
						<button type="button" onclick={() => removeUrlByIndex(i)}>
							Remove URL
						</button>
					{/snippet}
				</Control>
				<Description class="sr-only">
					This URL will be publicly available on your profile.
				</Description>
				<FieldErrors />
			</ElementField>
		{/each}
		<FieldErrors />
		<button type="button" onclick={addUrl}>Add URL</button>
	</Fieldset>
 
	<button type="submit">Submit</button>
</form>
	

We've added a removeUrlByIndex function that removes a URL from the urls array by its index, and a addUrl function that adds a new URL to the urls array. We've also added a button to remove each URL and a button to add a new URL.

Now the user can add and remove URLs as needed, and the form will validate the array and its elements according to the schema we defined.

Finished Product

That's it! 🎉

You've created a dynamic field that allows users to add and remove URLs from their profile. With some custom styles and finesse, you can make the form look something like this:

Public URLs
This URL will be displayed on your public profile.
This URL will be displayed on your public profile.

TLDR - Show Me the Code

Here's the complete code for the form we built in this guide:

schema.ts
		import { z } from "zod";
 
export const schema = z.object({
	urls: z
		.array(z.string().url({ message: "Please enter a valid URL." }))
		.min(2, "You must include at least two URLs on your profile.")
		.default(["", ""]),
});
	
+page.svelte
		<script lang="ts">
	import { superForm } from "sveltekit-superforms";
	import { zodClient } from "sveltekit-superforms/adapters";
	import {
		Fieldset,
		Legend,
		ElementField,
		Control,
		Label,
		FieldErrors,
		Description,
	} from "formsnap";
	import { schema } from "./schema.js";
 
	let { data } = $props();
 
	const form = superForm(data.form, {
		validators: zodClient(schema),
	});
 
	const { form: formData } = form;
 
	function removeUrlByIndex(index: number) {
		$formData.urls = $formData.urls.filter((_, i) => i !== index);
	}
 
	function addUrl() {
		$formData.urls = [...$formData.urls, ""];
	}
</script>
 
<form use:form.enhance method="POST">
	<Fieldset {form} name="urls">
		<Legend>Public URLs</Legend>
		{#each $formData.urls as _, i}
			<ElementField {form} name="urls[{i}]">
				<Control>
					{#snippet children({ props })}
						<Label class="sr-only">URL {i + 1}</Label>
						<input type="url" {...props} bind:value={$formData.urls[i]} />
						<button type="button" onclick={() => removeUrlByIndex(i)}>
							Remove URL
						</button>
					{/snippet}
				</Control>
				<Description class="sr-only">
					This URL will be publicly available on your profile.
				</Description>
			</ElementField>
		{/each}
		<FieldErrors />
		<button type="button" onclick={addUrl}>Add URL</button>
	</Fieldset>
 
	<button type="submit">Submit</button>
</form>