Действия формы
Файл +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).
<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` - функция, запускающая стандартную логику (которая выполнилась бы без этого колбэка) }; }}>Эти функции можно использовать для отображения/скрытия индикаторов загрузки и т. д.
Если вы возвращаете колбэк, вы переопределяете стандартное поведение после отправки формы. Чтобы восстановить его, вызовите update (который принимает параметры invalidateAll и reset) или используйте 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> для управления поведением роутера.