diff --git a/.changeset/spicy-pugs-applaud.md b/.changeset/spicy-pugs-applaud.md new file mode 100644 index 000000000000..fe04a4ca8d6b --- /dev/null +++ b/.changeset/spicy-pugs-applaud.md @@ -0,0 +1,6 @@ +--- +'create-svelte': patch +'@sveltejs/kit': patch +--- + +[breaking] Replace `POST`/`PUT`/`PATCH`/`DELETE` in `+page.server.js` with `export const actions` diff --git a/.prettierrc b/.prettierrc index 478c4dcc8747..5fb53ba46fda 100644 --- a/.prettierrc +++ b/.prettierrc @@ -21,6 +21,7 @@ "files": [ "**/CHANGELOG.md", "**/.svelte-kit/**", + "documentation/**/*.md", "packages/package/test/fixtures/**/expected/**/*", "packages/package/test/watch/expected/**/*", "packages/package/test/watch/package/**/*", diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md index 2dc453bd2492..7c17d59a01e9 100644 --- a/documentation/docs/03-routing.md +++ b/documentation/docs/03-routing.md @@ -112,74 +112,7 @@ During client-side navigation, SvelteKit will load this data from the server, wh Like `+page.js`, `+page.server.js` can export [page options](/docs/page-options) — `prerender`, `ssr` and `csr`. -#### Actions - -`+page.server.js` can also declare _actions_, which correspond to the `POST`, `PATCH`, `PUT` and `DELETE` HTTP methods. A request made to the page with one of these methods will invoke the corresponding action before rendering the page. - -An action can return a `{ status?, errors }` object if there are validation errors (`status` defaults to `400`), or an optional `{ location }` object to redirect the user to another page: - -```js -/// file: src/routes/login/+page.server.js - -// @filename: ambient.d.ts -declare global { - const createSessionCookie: (userid: string) => string; - const hash: (content: string) => string; - const db: { - findUser: (name: string) => Promise<{ - id: string; - username: string; - password: string; - }> - } -} - -export {}; - -// @filename: index.js -// ---cut--- -import { error } from '@sveltejs/kit'; - -/** @type {import('./$types').Action} */ -export async function POST({ cookies, request, url }) { - const values = await request.formData(); - - const username = /** @type {string} */ (values.get('username')); - const password = /** @type {string} */ (values.get('password')); - - const user = await db.findUser(username); - - if (!user) { - return { - status: 403, - errors: { - username: 'No user with this username' - } - }; - } - - if (user.password !== hash(password)) { - return { - status: 403, - errors: { - password: 'Incorrect password' - } - }; - } - - cookies.set('sessionid', createSessionCookie(user.id), { - httpOnly: true - }); - - return { - location: url.searchParams.get('redirectTo') ?? '/' - }; -} -``` - -If validation `errors` are returned, they will be available inside `+page.svelte` as `export let errors`. - -> The actions API will likely change in the near future: https://github.com/sveltejs/kit/discussions/5875 +A `+page.server.js` file can also export _actions_. If `load` lets you read data from the server, `actions` let you write data _to_ the server using the `
` element. To learn how to use them, see the [form actions](/docs/form-actions) section. ### +error diff --git a/documentation/docs/06-form-actions.md b/documentation/docs/06-form-actions.md new file mode 100644 index 000000000000..cc11ab9e9387 --- /dev/null +++ b/documentation/docs/06-form-actions.md @@ -0,0 +1,347 @@ +--- +title: Form Actions +--- + +A `+page.server.js` file can export _actions_, which allow you to `POST` data to the server using the `` element. + +When using ``, client-side JavaScript is optional, but you can easily _progressively enhance_ your form interactions with JavaScript to provide the best user experience. + +### Default actions + +In the simplest case, a page declares a `default` action: + +```js +/// file: src/routes/login/+page.server.js +/** @type {import('./$types').Actions} */ +export const actions = { + default: async (event) => { + // TODO log the user in + } +}; +``` + +To invoke this action from the `/login` page, just add a `` — no JavaScript needed: + +```svelte +/// file: src/routes/login/+page.svelte + + + + +
+``` + +If someone were to click the button, the browser would send the form data via `POST` request to the server, running the default action. + +> Actions always use `POST` requests, since `GET` requests should never have side-effects. + +We can also invoke the action from other pages (for example if there's a login widget in the nav in the root layout) by adding the `action` attribute, pointing to the page: + +```html +/// file: src/routes/+layout.svelte +
+ +
+``` + +### Named actions + +In addition to `default` actions, a page can have as many named actions as it needs: + +```diff +/// file: src/routes/login/+page.server.js + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async (event) => { + // TODO log the user in + }, ++ register: async (event) => { ++ // TODO register the user ++ } +}; +``` + +To invoke a named action, add a query parameter with the name prefixed by a `/` character: + +```svelte +/// file: src/routes/login/+page.svelte +
+``` + +```svelte +/// file: src/routes/+layout.svelte + +``` + +As well as the `action` attribute, we can use the `formaction` attribute on a button to `POST` the same form data to a different action than the parent ``: + +```diff +/// file: src/routes/login/+page.svelte + + + + ++ +
+``` + +### Anatomy of an action + +Each action receives a `RequestEvent` object, allowing you to read the data with `request.formData()`. After processing the request (for example, logging the user in by setting a cookie), the action can respond with data that will be available as `form` until the next update. + +```js +// @errors: 2339 2304 +/// file: src/routes/login/+page.server.js +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ cookies, request }) => { + const data = await request.formData(); + const email = data.get('email'); + const password = data.get('password'); + + const user = await db.getUser(email); + cookies.set('sessionid', await db.createSession(user)); + + return { success: true }; + }, + register: async (event) => { + // TODO register the user + } +}; +``` + +```svelte +/// file: src/routes/login/+page.svelte + + +{#if form?.success} + +

Successfully logged in! Welcome back, {data.user.name}

+{/if} +``` + +#### Validation errors + +If the request couldn't be processed because of invalid data, you can return validation errors — along with the previously submitted form values — back to the user so that they can try again. The `invalid` function lets you return an HTTP status code (typically 400, in the case of validation errors) along with the data: + +```diff +// @errors: 2339 2304 +/// file: src/routes/login/+page.server.js ++import { invalid } from '@sveltejs/kit'; + +/** @type {import('./$types').Actions} */ +export const actions = { + default: async ({ cookies, request }) => { + const data = await request.formData(); + const email = data.get('email'); + const password = data.get('password'); + + const user = await db.getUser(email); ++ if (!user) { ++ return invalid(400, { email, missing: true }); ++ } ++ ++ if (user.password !== hash(password)) { ++ return invalid(400, { email, incorrect: true }); ++ } + + cookies.set('sessionid', await db.createSession(user)); + + return { success: true }; + }, + register: async (event) => { + // TODO register the user + } +}; +``` + +> Note that as a precaution, we only return the email back to the page — not the password. + +```diff +/// file: src/routes/login/+page.svelte +
+- ++ {#if form?.missing}

No user found with this email

{/if} ++ + +- ++ {#if form?.incorrect}

Wrong password!

{/if} + + + +
+``` + +The returned data must be serializable as JSON. Beyond that, the structure is entirely up to you. For example, if you had multiple forms on the page, you could distinguish which `
` the returned `form` data referred to with an `id` property or similar. + +#### Redirects + +Redirects (and errors) work exactly the same as in [`load`](/docs/load#redirects): + +```diff +// @errors: 2339 2304 +/// file: src/routes/login/+page.server.js ++import { invalid, redirect } from '@sveltejs/kit'; + +/** @type {import('./$types').Actions} */ +export const actions = { ++ default: async ({ cookies, request, url }) => { + const data = await request.formData(); + const email = data.get('email'); + const password = data.get('password'); + + const user = await db.getUser(email); + if (!user) { + return invalid(400, { email, missing: true }); + } + + if (user.password !== hash(password)) { + return invalid(400, { email, incorrect: true }); + } + + cookies.set('sessionid', await db.createSession(user)); + ++ if (url.searchParams.has('redirectTo')) { ++ throw redirect(303, url.searchParams.get('redirectTo')); ++ } + + return { success: true }; + }, + register: async (event) => { + // TODO register the user + } +}; +``` + +### Progressive enhancement + +In the preceding sections we built a `/login` action that [works without client-side JavaScript](https://kryogenix.org/code/browser/everyonehasjs.html) — not a `fetch` in sight. That's great, but when JavaScript _is_ available we can progressively enhance our form interactions to provide a better user experience. + +#### use:enhance + +The easiest way to progressively enhance a form is to add the `use:enhance` action: + +```diff +/// file: src/routes/login/+page.svelte + + ++ +``` + +> Yes, it's a little confusing that the `enhance` action and `` are both called 'action'. These docs are action-packed. Sorry. + +Without an argument, `use:enhance` will emulate the browser-native behaviour, just without the full-page reloads. It will: + +- update the `form` property and invalidate all data on a successful response +- update the `form` property on a invalid response +- update `$page.status` on a successful or invalid response +- call `goto` on a redirect response +- render the nearest `+error` boundary if an error occurs + +To customise the behaviour, you can provide a function that runs immediately before the form is submitted, and (optionally) returns a callback that runs with the `ActionResult`. + +```svelte + { + // `form` is the `` element + // `data` is its `FormData` object + // `cancel()` will prevent the submission + + return async (result) => { + // `result` is an `ActionResult` object + }; + }} +> +``` + +You can use these functions to show and hide loading UI, and so on. + +#### applyAction + +If you provide your own callbacks, you may need to reproduce part of the default `use:enhance` behaviour, such as showing the nearest `+error` boundary. We can do this with `applyAction`: + +```diff + + + { + // `form` is the `` element + // `data` is its `FormData` object + // `cancel()` will prevent the submission + + return async (result) => { + // `result` is an `ActionResult` object ++ if (result.type === 'error') { ++ await applyAction(result); ++ } + }; + }} +> +``` + +The behaviour of `applyAction(result)` depends on `result.type`: + +- `success`, `invalid` — sets `$page.status` to `result.status` and updates `form` to `result.data` +- `redirect` — calls `goto(result.location)` +- `error` — renders the nearest `+error` boundary with `result.error` + +#### Custom event listener + +We can also implement progressive enhancement ourselves, without `use:enhance`, with a normal event listener on the ``: + +```svelte +/// file: src/routes/login/+page.svelte + + + + +
+``` diff --git a/documentation/docs/06-hooks.md b/documentation/docs/07-hooks.md similarity index 100% rename from documentation/docs/06-hooks.md rename to documentation/docs/07-hooks.md diff --git a/documentation/docs/07-modules.md b/documentation/docs/08-modules.md similarity index 100% rename from documentation/docs/07-modules.md rename to documentation/docs/08-modules.md diff --git a/documentation/docs/08-service-workers.md b/documentation/docs/09-service-workers.md similarity index 100% rename from documentation/docs/08-service-workers.md rename to documentation/docs/09-service-workers.md diff --git a/documentation/docs/09-link-options.md b/documentation/docs/10-link-options.md similarity index 100% rename from documentation/docs/09-link-options.md rename to documentation/docs/10-link-options.md diff --git a/documentation/docs/10-adapters.md b/documentation/docs/11-adapters.md similarity index 100% rename from documentation/docs/10-adapters.md rename to documentation/docs/11-adapters.md diff --git a/documentation/docs/11-page-options.md b/documentation/docs/12-page-options.md similarity index 100% rename from documentation/docs/11-page-options.md rename to documentation/docs/12-page-options.md diff --git a/documentation/docs/12-packaging.md b/documentation/docs/13-packaging.md similarity index 100% rename from documentation/docs/12-packaging.md rename to documentation/docs/13-packaging.md diff --git a/documentation/docs/13-cli.md b/documentation/docs/14-cli.md similarity index 100% rename from documentation/docs/13-cli.md rename to documentation/docs/14-cli.md diff --git a/documentation/docs/14-configuration.md b/documentation/docs/15-configuration.md similarity index 97% rename from documentation/docs/14-configuration.md rename to documentation/docs/15-configuration.md index f2fcfe0832a1..535aedb03cf2 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/15-configuration.md @@ -43,10 +43,6 @@ const config = { errorTemplate: 'src/error.html' }, inlineStyleThreshold: 0, - methodOverride: { - parameter: '_method', - allowed: [] - }, moduleExtensions: ['.js', '.ts'], outDir: '.svelte-kit', paths: { @@ -196,13 +192,6 @@ Inline CSS inside a `