-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Form actions #6469
Merged
Merged
Form actions #6469
Changes from 19 commits
Commits
Show all changes
93 commits
Select commit
Hold shift + click to select a range
1c3ba7a
basic actions method
dummdidumm 9ac19bc
add form store
dummdidumm 7b71fa3
remove export let errors in favor of form store
dummdidumm d880096
make actions an object
dummdidumm 102851b
implement FilesFormData
dummdidumm e650cb3
restrict values type, fix conversion, switch argument order
dummdidumm 678361d
Merge branch 'master' into form-actions
dummdidumm 5ece9c0
validation->invalid
dummdidumm 9c9dfbe
start writing docs
dummdidumm b11c578
implement handleFile
dummdidumm 3450eb4
fix tests
dummdidumm 38a253c
test for files
dummdidumm 2da72d9
complete migration message
dummdidumm 62fa93c
support validation error thrown in endpoints
dummdidumm 225be5c
infer file type from handleFile hook
dummdidumm 7ce58ea
add handleFile to build
dummdidumm 9dcb6be
types, cleanup
dummdidumm f1941a5
$form -> $submitted
dummdidumm 71ed352
woops
dummdidumm ba0f204
allow arbitrary data on invalid, persist data in success case
dummdidumm f6be2c5
fix infered FileType
dummdidumm d8e7137
give JSON response a well-defined shape
dummdidumm b2e30b9
provide form state through form prop and $page.form
dummdidumm 29294d0
return invalid instead of throwing it
dummdidumm ba4aa8f
types for actions
dummdidumm be35a3a
Merge remote-tracking branch 'origin/master' into form-actions
dummdidumm 855c1b8
fix, skip test
dummdidumm 09c10af
Merge remote-tracking branch 'origin/master' into form-actions
dummdidumm 2a14eaf
updateForm (simple version)
dummdidumm 792b659
making a start at enhance
dummdidumm ca3cae9
bye bye method overrides
dummdidumm 8d9fe0b
update create-svelte default template
dummdidumm 823153f
Merge remote-tracking branch 'origin/master' into form-actions
dummdidumm 6213bf0
remove handleFile
dummdidumm 646013b
full blown enhance and updateForm through $app/forms
dummdidumm d82933f
fix type reference
dummdidumm 16f1ad4
adjust default template
dummdidumm 9eef9a7
remove $page.form for now (too much of a footgun due to resets)
dummdidumm f17ef01
tests, ensure form is only reset on page changes
dummdidumm c208888
?????
dummdidumm f0d0222
cleanup
dummdidumm d43a085
docs about multiple forms
dummdidumm f3926c4
make docs build
dummdidumm 1045828
lint
Rich-Harris 2123945
fix
Rich-Harris e1aadca
add toggle action
Rich-Harris b19bb4b
rename generated FormData type to ActionData
Rich-Harris 2647a10
reset form on navigation, not invalidation
Rich-Harris 0eb68e2
silence missing/unused form prop warnings, DRY out code a bit
Rich-Harris 6fe185a
Update packages/kit/src/runtime/server/page/actions.js
Rich-Harris 21b9f46
Update packages/kit/src/runtime/server/page/actions.js
Rich-Harris 7bc739e
Update packages/kit/src/runtime/server/page/actions.js
Rich-Harris c3c8369
change message to reference actions
Rich-Harris 732093c
Update packages/kit/src/runtime/server/page/actions.js
Rich-Harris 333b87a
Update packages/kit/src/runtime/server/page/actions.js
Rich-Harris b561522
be more specific about what content-type is accepted
Rich-Harris e17acfd
Update packages/kit/src/runtime/server/page/render.js
Rich-Harris a3a21ca
Update packages/kit/src/runtime/server/page/render.js
Rich-Harris 260e0d4
unskip test
Rich-Harris fa78907
Merge branch 'form-actions' of github.com:sveltejs/kit into form-actions
Rich-Harris 7c8c360
fix types
dummdidumm b081e8c
tweak enhance function and make todos example work again
dummdidumm 60b8341
changeset
dummdidumm 3be2e9e
deduplicate type usage
dummdidumm 4119273
merge
Rich-Harris 26908ba
lint
dummdidumm 34e515b
wording, make updateForm (now applySubmissionResult) more powerful
dummdidumm e62a6ea
fix, docs
dummdidumm 91b143c
applySubmissionResult -> applyAction
dummdidumm b94a89a
change enhance function signature
dummdidumm 5c15c23
fix template
dummdidumm 3275ebb
fix template
dummdidumm 45580ac
merge
Rich-Harris d11c059
Update .changeset/spicy-pugs-applaud.md
Rich-Harris 4198872
Update packages/kit/src/runtime/app/forms.js
Rich-Harris a09aad3
Apply suggestions from code review
Rich-Harris 840f714
prettier
Rich-Harris 7eb124b
rename SubmissionResult to ActionResult
Rich-Harris 7ff7e7f
Update packages/kit/types/ambient.d.ts
Rich-Harris ff023d4
Update packages/kit/types/ambient.d.ts
Rich-Harris 1b194bb
updateForm -> applyAction
Rich-Harris 7b6e988
use RequestEvent instead of ActionEvent
Rich-Harris 9385bd0
fix
Rich-Harris c393862
invalidate first, delegate redirect/error handling to applyAction
Rich-Harris bb910be
remove token stuff, simplify a bit
Rich-Harris e28d384
show +error page without reloading route
Rich-Harris 7ee1425
check action return data can be serialized as JSON
Rich-Harris 3906922
add note to render.js
Rich-Harris 0f1f5c3
merge FetchFormResponse and ActionResult
Rich-Harris d899b30
update docs
Rich-Harris 5ee4fcd
remove logging
Rich-Harris a0d7580
tiny docs tweak
dummdidumm f2cdab2
merge
Rich-Harris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
--- | ||
title: Form Actions | ||
--- | ||
|
||
`+page.server.js` can declare _actions_ which are specifically designed for form interactions. It enables things like preserving user input in case of a full page reload with validation errors while making progressive enhancement through JavaScript possible. | ||
|
||
## Defining actions by name | ||
|
||
Actions are defined through `export const actions = {...}`, with each key being the name of the action and the value being the function that is invoked when the form with that action is submitted. A `POST` request made to the page will invoke the corresponding action using a query parameter that start's with a `/` - so for example `POST todos?/addTodo` will invoke the `addTodo` action. The `default` action is called when no such query parameter is given. | ||
|
||
```svelte | ||
/// file: src/routes/todos/+page.svelte | ||
<script> | ||
/** @type {import('./$types').PageData} */ | ||
export let data; | ||
</script> | ||
|
||
<form action="?/addTodo" method="post"> | ||
<input type="text" name="text" /> | ||
<button>Add todo</button> | ||
</form> | ||
|
||
<ul> | ||
{#each data.todos as todo} | ||
<li> | ||
<form action="?/editTodo" method="post"> | ||
<input type="hidden" name="id" value={todo.id} /> | ||
<input type="text" name="text" value={todo.text} /> | ||
<button>Edit todo</button> | ||
</form> | ||
</li> | ||
{/each} | ||
</ul> | ||
``` | ||
|
||
```js | ||
/// file: src/routes/todos/+page.server.js | ||
/** @type {import('./$types').Actions} */ | ||
export const actions = { | ||
addTodo: (event) => { | ||
// ... | ||
}, | ||
editTodo: (event) => { | ||
// ... | ||
} | ||
}; | ||
``` | ||
|
||
## Files and strings are separated | ||
|
||
Since `actions` are meant to be used with forms, we can make your life easier by awaiting the `FormData` and separating the form fields which contain strings from those who contain files and running the latter through the [`handleFile`](/docs/hooks#handleFile) hook before passing it as `fields` and `files` into the action function. | ||
|
||
```js | ||
/// file: src/routes/todos/+page.server.js | ||
/** @type {import('./$types').Actions} */ | ||
export const actions = { | ||
default: ({ fields, files }) => { | ||
const name = fields.get('name'); // typed as string | ||
const image = files.get('image'); // typed as the return type of the handleFile hook | ||
// ... | ||
} | ||
}; | ||
``` | ||
|
||
## Validation | ||
|
||
A core part of form submissions is validation. For this, an action can `throw` the `invalid` helper method exported from `@sveltejs/kit` if there are validation errors. `invalid` expects a `status`, possibly the form `values` (make sure to remove any user sensitive information such as passwords) and an `error` object. In case of a native form submit they populate the `$submitted` store which is available inside your components so you can preserve user input. | ||
|
||
```js | ||
/// file: src/routes/login/+page.server.js | ||
|
||
// @filename: ambient.d.ts | ||
declare global { | ||
const db: { | ||
findUser: (name: string) => Promise<{ | ||
id: string; | ||
username: string; | ||
password: string; | ||
}> | ||
} | ||
} | ||
|
||
export {}; | ||
|
||
// @filename: index.js | ||
// ---cut--- | ||
import { invalid } from '@sveltejs/kit'; | ||
|
||
/** @type {import('./$types').Actions} */ | ||
export const actions = { | ||
default: async ({ fields, setHeaders, url }) => { | ||
const username = fields.get('username'); | ||
const password = fields.get('password'); | ||
|
||
const user = await db.findUser(username); | ||
|
||
if (!user) { | ||
throw invalid(403, { username }, { | ||
username: 'No user with this username' | ||
}); | ||
} | ||
|
||
// ... | ||
} | ||
}; | ||
``` | ||
|
||
```svelte | ||
/// file: src/routes/login/+page.svelte | ||
<script> | ||
import { submitted } from '$app/stores'; | ||
</script> | ||
|
||
<form action="?/addTodo" method="post"> | ||
<input type="text" name="username" value={$submitted?.values?.username} /> | ||
{#if $submitted?.errors?.username} | ||
<span>{$submitted?.errors?.username}</span> | ||
{/if} | ||
<input type="password" name="password" /> | ||
<button>Login</button> | ||
</form> | ||
``` | ||
|
||
## Success | ||
|
||
If everything is valid, an action can return a JSON object with data that is part of the JSON response in the case of a JavaScript fetch - it's discarded in case of a full page reload. Alternatively it can `throw` a `redirect` to redirect the user to another page. | ||
|
||
```js | ||
/// file: src/routes/login/+page.server.js | ||
import { redirect } from '@sveltejs/kit'; | ||
|
||
/** @type {import('./$types').Actions} */ | ||
export const actions = { | ||
default: async ({ url }) => { | ||
// ... | ||
|
||
if (url.searchParams.get('redirectTo')) { | ||
throw redirect(303, url.searchParams.get('redirectTo')); | ||
} else { | ||
return { | ||
success: true | ||
}; | ||
} | ||
} | ||
}; | ||
``` | ||
|
||
## Progressive enhancement | ||
|
||
So far, all the code examples run native form submissions - that is, when the user pressed the submit button, the page is reloaded. It's good that this use case is supported since JavaScript may not be loaded all the time. When it is though, it might be a better user experience to use the powers JavaScript gives us to provide a better user experience - this is called progressive enhancement. | ||
|
||
First we need to ensure that the page is _not_ reloaded on submission. For this, we prevent the default behavior. Afterwards, we run our JavaScript code instead which does the form submission through `fetch` instead. | ||
|
||
```svelte | ||
/// file: src/routes/login/+page.svelte | ||
<script> | ||
import { submitted } from '$app/stores'; | ||
import { invalidateAll, goto } from '$app/navigation'; | ||
|
||
async function login(event) { | ||
event.preventDefault(); // prevent native form submission | ||
const data = new FormData(this); // create FormData | ||
const response = await fetch(this.action, { // call action using fetch | ||
method: 'POST', | ||
headers: { | ||
accept: 'application/json' | ||
}, | ||
body: data | ||
}); | ||
const { errors, location, values } = await response.json(); // destructure response object | ||
if (response.ok) { // success, redirect | ||
invalidateAll(); | ||
goto(location); | ||
} else { // validation error, update $submitted store | ||
$submitted = { errors, values } }; | ||
} | ||
} | ||
</script> | ||
|
||
<form action="?/addTodo" method="post" on:submit|preventDefault={login}> | ||
<input type="text" name="username" value={$submitted?.values?.username} /> | ||
{#if $submitted?.errors.username} | ||
<span>{$submitted.errors.username}</span> | ||
{/if} | ||
<input type="password" name="password" /> | ||
<button>Login</button> | ||
</form> | ||
``` | ||
|
||
## `<Form>` component | ||
|
||
As you can see, progressive enhancement is doable, but it may become a little cumbersome over time. That's why we will soon provide an opinionated wrapper component which does all the heavy lifting for you. Here's how the same login page would look like using the `<Form>` component: | ||
|
||
```svelte | ||
/// file: src/routes/login/+page.svelte | ||
<script> | ||
import { Form } from '@sveltejs/kit'; | ||
import { goto } from '$app/navigation'; | ||
|
||
async function redirect({ location }) { | ||
goto(location); | ||
} | ||
</script> | ||
|
||
<Form action="?/addTodo" on:success={redirect} let:errors let:values> | ||
<input type="text" name="username" value={values?.username} /> | ||
{#if errors?.username} | ||
<span>{errors.username}</span> | ||
{/if} | ||
<input type="password" name="password" /> | ||
<button>Login</button> | ||
</Form> | ||
``` | ||
|
||
## Alternatives | ||
|
||
In case you don't need your forms to work without JavaScript, you want to use HTTP verbs other than `POST`, or you want to send arbitrary JSON instead of being restricted to `FormData`, then you can resort to interacting with your API through `+server.js` endpoints (which will be possible to place next to `+page` files, soon). | ||
|
||
```svelte | ||
<script> | ||
import { invalidateAll, goto } from '$app/navigation'; | ||
|
||
let errors = {}; | ||
|
||
async function login(event) { | ||
event.preventDefault(); // prevent native form submission | ||
const data = Object.fromEntries(new FormData(this)); // create JSON from FormData | ||
const response = await fetch('/api/login', { // call your API using fetch | ||
method: 'POST', | ||
headers: { | ||
accept: 'application/json' | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify(data) | ||
}); | ||
const json = await response.json(); // destructure response object | ||
if (response.ok) { // success, redirect | ||
invalidateAll(); | ||
goto(json.location); | ||
} else { // validation error, errors variable | ||
errors = json.errors; | ||
} | ||
} | ||
</script> | ||
|
||
<form on:submit|preventDefault={login}> | ||
<input type="text" name="username" /> | ||
{#if errors.username} | ||
<span>{errors.username}</span> | ||
{/if} | ||
<input type="password" name="password" /> | ||
<button>Login</button> | ||
</form> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we calling
preventDefault
again here?The form already has
on:submit|preventDefault={login}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, this should be deduplicated - not sure which way though.
|preventDefault
is more Svelte-like, but as a reader you may not have seen that before, and withevent.preventDefault
we can put an explanatory comment next to it - so I probably lean towards the latter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel
|preventDefault
looks clean and makes sense for docs to use more of svelte way.But yeah we loose comment option.