Действия формы
Файл +page.server.js
может экспортировать действия (actions), позволяющие отправлять данные на сервер методом POST
с помощью элемента <form>
.
При использовании <form>
клиентский JavaScript не обязателен, но вы можете легко постепенно улучшать взаимодействие с формой с помощью JavaScript для лучшей интерактивности.
Действия по умолчанию
Заголовок раздела «Действия по умолчанию»В простейшем случае страница объявляет действие default
:
/** @satisfies {import('./$types').Actions} */export const actions = { default: async (event) => { // TODO авторизовываем пользователя }};
import type { Actions } from './$types';
export const actions = { default: async (event) => { // TODO авторизовываем пользователя }} satisfies Actions;
Чтобы вызвать это действие со страницы /login
, просто добавьте <form>
— JavaScript не требуется:
<form method="POST"> <label> Имейл <input name="email" type="email"> </label> <label> Пароль <input name="password" type="password"> </label> <button>Войти</button></form>
При нажатии кнопки браузер отправит данные формы на сервер через POST
-запрос, выполняя действие по умолчанию.
Действие также можно вызвать с других страниц (например, если в навигации корневого макета есть виджет входа), указав атрибут action
с путём к странице:
<form method="POST" action="/login"> <!-- содержимое --></form>
Именованные действия
Заголовок раздела «Именованные действия»Вместо одного действия default
страница может содержать несколько именованных действий:
/** @satisfies {import('./$types').Actions} */export const actions = { default: async (event) => { login: async (event) => { // TODO авторизовываем пользователя }, register: async (event) => { // TODO регистрируем пользователя }};
import type { Actions } from './$types';
export const actions = { default: async (event) => { login: async (event) => { // TODO авторизовываем пользователя }, register: async (event) => { // TODO регистрируем пользователя }} satisfies Actions;
Для вызова именованного действия добавьте query-параметр с именем, начинающимся с символа /
:
<form method="POST" action="?/register">
<form method="POST" action="/login?/register">
Помимо атрибута action
, можно использовать атрибут formaction
на кнопке, чтобы отправить те же данные формы в другое действие, отличное от указанного в родительском элементе <form>
:
<form method="POST" action="?/login"> <label> Имейл <input name="email" type="email"> </label> <label> Пароль <input name="password" type="password"> </label> <button>Войти</button> <button formaction="?/register">Зарегистрироваться</button></form>
Структура действия
Заголовок раздела «Структура действия»Каждое действие получает объект RequestEvent
, позволяющий:
- Читать данные через
request.formData()
- Обрабатывать запрос (например, авторизовать пользователя, установив куки)
- Возвращать данные, которые станут доступны:
- Через свойство
form
на соответствующей странице - Через
page.form
во всём приложении до следующего обновления
- Через свойство
import * as db from '$lib/server/db';
/** @type {import('./$types').PageServerLoad} */export async function load({ cookies }) { const user = await db.getUserFromSession(cookies.get('sessionid')); return { user };}
/** @satisfies {import('./$types').Actions} */export const actions = { login: 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), { path: '/' });
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }};
import * as db from '$lib/server/db';import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { const user = await db.getUserFromSession(cookies.get('sessionid')); return { user };};export const actions = { login: 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), { path: '/' });
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }} satisfies Actions;
<script> /** @type {import('./$types').PageProps} */ let { data, form } = $props();</script>
{#if form?.success} <!-- это временное сообщение; оно существует потому, что страница была отрисована в ответ на отправку формы. оно исчезнет при обновлении страницы --> <p>Успешный вход! С возвращением, {data.user.name}</p>{/if}
<script lang="ts"> import type { PageProps } from './$types';
let { data, form }: PageProps = $props();</script>
{#if form?.success} <!-- это временное сообщение; оно существует потому, что страница была отрисована в ответ на отправку формы. оно исчезнет при обновлении страницы --> <p>Успешный вход! С возвращением, {data.user.name}</p>{/if}
Ошибки валидации
Заголовок раздела «Ошибки валидации»Если запрос не может быть обработан из-за неверных данных, вы можете вернуть ошибки валидации вместе с ранее отправленными значениями формы, чтобы пользователь мог попробовать снова. Функция fail
позволяет вернуть HTTP-статус (обычно 400 или 422 для ошибок валидации) вместе с данными. Код статуса доступен через page.status
, а данные через form
:
import { fail } from '@sveltejs/kit';import * as db from '$lib/server/db';
/** @satisfies {import('./$types').Actions} */export const actions = { login: async ({ cookies, request }) => { const data = await request.formData(); const email = data.get('email'); const password = data.get('password');
if (!email) { return fail(400, { email, missing: true }); }
const user = await db.getUser(email);
if (!user || user.password !== db.hash(password)) { return fail(400, { email, incorrect: true }); }
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }};
import { fail } from '@sveltejs/kit';import * as db from '$lib/server/db';import type { Actions } from './$types';
export const actions = { login: async ({ cookies, request }) => { const data = await request.formData(); const email = data.get('email'); const password = data.get('password');
if (!email) { return fail(400, { email, missing: true }); }
const user = await db.getUser(email);
if (!user || user.password !== db.hash(password)) { return fail(400, { email, incorrect: true }); }
cookies.set('sessionid', await db.createSession(user), { path: '/' });
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }} satisfies Actions;
<form method="POST" action="?/login"> {#if form?.missing}<p class="error">Имейл обязателен</p>{/if} {#if form?.incorrect}<p class="error">Неверные учётные данные!</p>{/if} <label> Имейл <input name="email" type="email" value={form?.email ?? ''}> </label> <label> Пароль <input name="password" type="password"> </label> <button>Войти</button> <button formaction="?/register">Зарегистрироваться</button></form>
Возвращаемые данные должны быть сериализуемы в JSON. Кроме этого требования, структура полностью зависит от вас. Например, если на странице несколько форм, можно различать их с помощью свойства id
или аналогичного.
Перенаправления
Заголовок раздела «Перенаправления»Перенаправления (и ошибки) работают так же, как в load
:
import { fail, redirect } from '@sveltejs/kit';import * as db from '$lib/server/db';
/** @satisfies {import('./$types').Actions} */export const actions = { login: 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 fail(400, { email, missing: true }); }
if (user.password !== db.hash(password)) { return fail(400, { email, incorrect: true }); }
cookies.set('sessionid', await db.createSession(user), { path: '/' });
if (url.searchParams.has('redirectTo')) { redirect(303, url.searchParams.get('redirectTo')); }
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }};
import { fail, redirect } from '@sveltejs/kit';import * as db from '$lib/server/db';import type { Actions } from './$types';
export const actions = { login: 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 fail(400, { email, missing: true }); }
if (user.password !== db.hash(password)) { return fail(400, { email, incorrect: true }); }
cookies.set('sessionid', await db.createSession(user), { path: '/' });
if (url.searchParams.has('redirectTo')) { redirect(303, url.searchParams.get('redirectTo')); }
return { success: true }; }, register: async (event) => { // TODO регистрируем пользователя }} satisfies Actions;
Загрузка данных
Заголовок раздела «Загрузка данных»После выполнения действия страница перерисовывается (если не произошло перенаправление или непредвиденная ошибка), а результат действия становится доступен странице через проп form
. Это означает, что функции load
страницы выполняются после завершения действия.
Обратите внимание, что handle
выполняется до вызова действия и не запускается повторно перед функциями load
. Следовательно, если вы используете handle
для заполнения event.locals
на основе куки, вы должны обновлять event.locals
при установке или удалении куки в действии:
/** @type {import('@sveltejs/kit').Handle} */export async function handle({ event, resolve }) { event.locals.user = await getUser(event.cookies.get('sessionid')); return resolve(event);}
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => { event.locals.user = await getUser(event.cookies.get('sessionid')); return resolve(event);};
/** @type {import('./$types').PageServerLoad} */export function load(event) { return { user: event.locals.user };}
/** @satisfies {import('./$types').Actions} */export const actions = { logout: async (event) => { event.cookies.delete('sessionid', { path: '/' }); event.locals.user = null; }};
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = (event) => { return { user: event.locals.user };};export const actions = { logout: async (event) => { event.cookies.delete('sessionid', { path: '/' }); event.locals.user = null; }} satisfies Actions;
Прогрессивное улучшение
Заголовок раздела «Прогрессивное улучшение»В предыдущих разделах мы создали действие /login
, которое работает без клиентского JavaScript — без единого fetch
. Это отлично, но когда JavaScript доступен, мы можем постепенно улучшать взаимодействие с формами для лучшей интерактивности.
use:enhance
Заголовок раздела «use:enhance»Самый простой способ прогрессивного улучшения формы — добавить действие use:enhance
:
<script> import { enhance } from '$app/forms';
/** @type {import('./$types').PageProps} */ let { form } = $props();</script>
<form method="POST" use:enhance>
<script lang="ts"> import { enhance } from '$app/forms'; import type { PageProps } from './$types';
let { form }: PageProps = $props();</script>
<form method="POST" use:enhance>
Без аргументов use:enhance
эмулирует нативное поведение браузера, но без перезагрузки страницы. Оно:
- Обновляет
form
,page.form
иpage.status
при успешном ответе или ошибке валидации (только если действие на текущей странице) - Сбрасывает форму
- Инвалидирует все данные через
invalidateAll
при успехе - Вызывает
goto
при редиректе - Показывает ближайшую границу
+error
при ошибке - Восстанавливает фокус
Настройка use:enhance
Заголовок раздела «Настройка use:enhance»Для кастомизации можно передать SubmitFunction
, которая:
- Выполняется перед отправкой формы
- Может вернуть колбэк (получающий
ActionResult
)
При возврате колбэка стандартное поведение отключается. Для его восстановления вызовите update
.
<form method="POST" use:enhance={({ formElement, formData, action, cancel, submitter }) => { // `formElement` - текущий элемент `<form>` // `formData` - объект `FormData`, который будет отправлен // `action` - URL, на который отправляется форма // вызов `cancel()` предотвратит отправку // `submitter` - `HTMLElement`, инициировавший отправку формы
return async ({ result, update }) => { // `result` - объект `ActionResult` // `update` - функция, запускающая стандартную логику (которая выполнилась бы без этого колбэка) }; }}>
Эти функции можно использовать для отображения/скрытия индикаторов загрузки и т. д.
При возврате колбэка может потребоваться воспроизвести часть стандартного поведения use:enhance
, но без инвалидации данных. Это можно сделать через applyAction
:
<script> import { enhance, applyAction } from '$app/forms';
/** @type {import('./$types').PageProps} */ let { form } = $props();</script>
<form method="POST" use:enhance={({ formElement, formData, action, cancel }) => { return async ({ result }) => { // `result` — объект `ActionResult` if (result.type === 'redirect') { goto(result.location); } else { await applyAction(result); } }; }}>
<script lang="ts"> import { enhance, applyAction } from '$app/forms'; import type { PageProps } from './$types';
let { form }: PageProps = $props();</script>
<form method="POST" use:enhance={({ formElement, formData, action, cancel }) => { return async ({ result }) => { // `result` — объект `ActionResult` if (result.type === 'redirect') { goto(result.location); } else { await applyAction(result); } }; }}>
Поведение applyAction(result)
зависит от result.type
:
success
,failure
— устанавливаетpage.status
вresult.status
и обновляетform
иpage.form
доresult.data
(в отличие отupdate
вenhance
, работает независимо от страницы отправки)redirect
— вызываетgoto(result.location, { invalidateAll: true })
error
— отображает ближайшую границу+error
сresult.error
Во всех случаях фокус будет сброшен.
Пользовательский обработчик событий
Заголовок раздела «Пользовательский обработчик событий»Мы также можем реализовать прогрессивное улучшение вручную, без use:enhance
, используя стандартный обработчик событий для <form>
:
<script> import { invalidateAll, goto } from '$app/navigation'; import { applyAction, deserialize } from '$app/forms';
/** @type {import('./$types').PageProps} */ let { form } = $props();
/** @param {SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}} event */ async function handleSubmit(event) { event.preventDefault(); const data = new FormData(event.currentTarget);
const response = await fetch(event.currentTarget.action, { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */ const result = deserialize(await response.text());
if (result.type === 'success') { // перезапускаем все функции `load` после успешного обновления await invalidateAll(); }
applyAction(result); }</script>
<form method="POST" onsubmit={handleSubmit}> <!-- содержимое --></form>
<script lang="ts"> import { invalidateAll, goto } from '$app/navigation'; import { applyAction, deserialize } from '$app/forms'; import type { PageProps } from './$types'; import type { ActionResult } from '@sveltejs/kit';
let { form }: PageProps = $props(); async function handleSubmit(event: SubmitEvent & { currentTarget: EventTarget & HTMLFormElement}) { event.preventDefault(); const data = new FormData(event.currentTarget);
const response = await fetch(event.currentTarget.action, { method: 'POST', body: data }); const result: ActionResult = deserialize(await response.text());
if (result.type === 'success') { // перезапускаем все функции `load` после успешного обновления await invalidateAll(); }
applyAction(result); }</script>
<form method="POST" onsubmit={handleSubmit}> <!-- содержимое --></form>
Обратите внимание, что перед обработкой ответа необходимо выполнить deserialize
из $app/forms
. Одного JSON.parse()
недостаточно, так как действия форм (как и функции load
) поддерживают возврат объектов Date
и BigInt
.
Если у вас есть +server.js
рядом с +page.server.js
, fetch
-запросы по умолчанию направляются туда. Для отправки POST
в действие из +page.server.js
используйте кастомный заголовок x-sveltekit-action
:
const response = await fetch(this.action, { method: 'POST', body: data, headers: { 'x-sveltekit-action': 'true' }});
Альтернативы
Заголовок раздела «Альтернативы»Действия форм — предпочтительный способ отправки данных на сервер благодаря возможности прогрессивного улучшения, но вы также можете использовать файлы +server.js
для создания (например) JSON API. Вот как может выглядеть такое взаимодействие:
<script> function rerun() { fetch('/api/ci', { method: 'POST' }); }</script>
<button onclick={rerun}>Перезапуск CI</button>
<script lang="ts"> function rerun() { fetch('/api/ci', { method: 'POST' }); }</script>
<button onclick={rerun}>Перезапуск CI</button>
/** @type {import('./$types').RequestHandler} */export function POST() { // что-то делаем}
import type { RequestHandler } from './$types';export const POST: RequestHandler = () => { // что-то делаем};
GET против POST
Заголовок раздела «GET против POST»Как мы видели, для вызова действия формы необходимо использовать method="POST"
.
Некоторым формам не нужно отправлять (POST
) данные на сервер — например, формам поиска. Для них можно использовать method="GET"
(или вообще не указывать метод), и SvelteKit будет обрабатывать их как элементы <a>
, используя клиентский роутер вместо полной перезагрузки страницы:
<form action="/search"> <label> Поиск <input name="q"> </label></form>
Отправка этой формы приведёт к переходу на /search?q=...
и вызову вашей функции load
, но не вызовет действие. Как и с элементами <a>
, вы можете использовать атрибуты data-sveltekit-reload
, data-sveltekit-replacestate
, data-sveltekit-keepfocus
и data-sveltekit-noscroll
на элементе <form>
для управления поведением роутера.