Skip to main content

Because we're using <form>, our app works even if the user doesn't have JavaScript (which happens more often than you probably think). That's great, because it means our app is resilient.

Most of the time, users do have JavaScript. In those cases, we can progressively enhance the experience, the same way SvelteKit progressively enhances <a> elements by using client-side routing.

Import the enhance function from $app/forms...

src/routes/+page.svelte
<script>
	import { enhance } from '$app/forms';

	export let data;
	export let form;
</script>

...and add the use:enhance directive to the <form> elements:

src/routes/+page.svelte
<form method="POST" action="?/create" use:enhance>
src/routes/+page.svelte
<form method="POST" action="?/delete" use:enhance>

And that's all it takes! Now, when JavaScript is enabled, use:enhance will emulate the browser-native behaviour except for the full-page reloads. It will:

  • update the form prop
  • invalidate all data on a successful response, causing load functions to re-run
  • navigate to the new page on a redirect response
  • render the nearest error page if an error occurs

Now that we're updating the page rather than reloading it, we can get fancy with things like transitions:

src/routes/+page.svelte
<script>
	import { fly, slide } from 'svelte/transition';
	import { enhance } from '$app/forms';

	export let data;
	export let form;
</script>
src/routes/+page.svelte
<li in:fly={{ y: 20 }} out:slide>...</li>

Next: Customizing use:enhance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<script>
	export let data;
	export let form;
</script>
 
<div class="centered">
	<h1>todos</h1>
 
	{#if form?.error}
		<p class="error">{form.error}</p>
	{/if}
 
	<form method="POST" action="?/create">
		<label>
			add a todo:
			<input
				name="description"
				value={form?.description ?? ''}
				autocomplete="off"
				required
			/>
		</label>
	</form>
 
	<ul class="todos">
		{#each data.todos as todo (todo.id)}
			<li>
				<form method="POST" action="?/delete">
					<input type="hidden" name="id" value={todo.id} />
					<span>{todo.description}</span>
					<button aria-label="Mark as complete"></button>
				</form>
			</li>
		{/each}
	</ul>
</div>
 
<style>
	.centered {
		max-width: 20em;
		margin: 0 auto;
	}
 
	label {
		width: 100%;
	}
 
	input {
		flex: 1;
	}
 
	span {
		flex: 1;
	}
 
	button {
		border: none;
		background: url(./remove.svg) no-repeat 50% 50%;
		background-size: 1rem 1rem;
		cursor: pointer;
		height: 100%;
		aspect-ratio: 1;
		opacity: 0.5;
		transition: opacity 0.2s;
	}
 
	button:hover {
		opacity: 1;
	}
 
	.saving {
		opacity: 0.5;
	}
</style>
 
initialising