Перейти к содержимому

Действия формы

Файл +page.server.js может экспортировать действия (actions), позволяющие отправлять данные на сервер методом POST с помощью элемента <form>.

При использовании <form> клиентский JavaScript не обязателен, но вы можете легко постепенно улучшать взаимодействие с формой с помощью JavaScript для лучшей интерактивности.

В простейшем случае страница объявляет действие default:

src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async (event) => {
// TODO авторизовываем пользователя
}
};

Чтобы вызвать это действие со страницы /login, просто добавьте <form> — JavaScript не требуется:

src/routes/login/+page.svelte
<form method="POST">
<label>
Имейл
<input name="email" type="email">
</label>
<label>
Пароль
<input name="password" type="password">
</label>
<button>Войти</button>
</form>

При нажатии кнопки браузер отправит данные формы на сервер через POST-запрос, выполняя действие по умолчанию.

Действие также можно вызвать с других страниц (например, если в навигации корневого макета есть виджет входа), указав атрибут action с путём к странице:

src/routes/+layout.svelte
<form method="POST" action="/login">
<!-- содержимое -->
</form>

Вместо одного действия default страница может содержать несколько именованных действий:

src/routes/login/+page.server.js
/** @satisfies {import('./$types').Actions} */
export const actions = {
default: async (event) => {
login: async (event) => {
// TODO авторизовываем пользователя
},
register: async (event) => {
// TODO регистрируем пользователя
}
};

Для вызова именованного действия добавьте query-параметр с именем, начинающимся с символа /:

src/routes/login/+page.svelte
<form method="POST" action="?/register">
src/routes/+layout.svelte
<form method="POST" action="/login?/register">

Помимо атрибута action, можно использовать атрибут formaction на кнопке, чтобы отправить те же данные формы в другое действие, отличное от указанного в родительском элементе <form>:

src/routes/login/+page.svelte
<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 во всём приложении до следующего обновления
src/routes/login/+page.server.js
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 регистрируем пользователя
}
};
src/routes/login/+page.svelte
<script>
/** @type {import('./$types').PageProps} */
let { data, form } = $props();
</script>
{#if form?.success}
<!-- это временное сообщение; оно существует потому, что страница была отрисована
в ответ на отправку формы. оно исчезнет при обновлении страницы -->
<p>Успешный вход! С возвращением, {data.user.name}</p>
{/if}

Если запрос не может быть обработан из-за неверных данных, вы можете вернуть ошибки валидации вместе с ранее отправленными значениями формы, чтобы пользователь мог попробовать снова. Функция fail позволяет вернуть HTTP-статус (обычно 400 или 422 для ошибок валидации) вместе с данными. Код статуса доступен через page.status, а данные через form:

src/routes/login/+page.server.js
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 регистрируем пользователя
}
};
src/routes/login/+page.svelte
<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:

src/routes/login/+page.server.js
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 регистрируем пользователя
}
};

После выполнения действия страница перерисовывается (если не произошло перенаправление или непредвиденная ошибка), а результат действия становится доступен странице через проп form. Это означает, что функции load страницы выполняются после завершения действия.

Обратите внимание, что handle выполняется до вызова действия и не запускается повторно перед функциями load. Следовательно, если вы используете handle для заполнения event.locals на основе куки, вы должны обновлять event.locals при установке или удалении куки в действии:

src/hooks.server.js
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.cookies.get('sessionid'));
return resolve(event);
}
src/routes/account/+page.server.js
/** @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;
}
};

В предыдущих разделах мы создали действие /login, которое работает без клиентского JavaScript — без единого fetch. Это отлично, но когда JavaScript доступен, мы можем постепенно улучшать взаимодействие с формами для лучшей интерактивности.

Самый простой способ прогрессивного улучшения формы — добавить действие use:enhance:

src/routes/login/+page.svelte
<script>
import { enhance } from '$app/forms';
/** @type {import('./$types').PageProps} */
let { form } = $props();
</script>
<form method="POST" use:enhance>

Без аргументов use:enhance эмулирует нативное поведение браузера, но без перезагрузки страницы. Оно:

  1. Обновляет form, page.form и page.status при успешном ответе или ошибке валидации (только если действие на текущей странице)
  2. Сбрасывает форму
  3. Инвалидирует все данные через invalidateAll при успехе
  4. Вызывает goto при редиректе
  5. Показывает ближайшую границу +error при ошибке
  6. Восстанавливает фокус

Для кастомизации можно передать 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:

src/routes/login/+page.svelte
<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);
}
};
}}
>

Поведение 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>:

src/routes/login/+page.svelte
<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>

Обратите внимание, что перед обработкой ответа необходимо выполнить 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. Вот как может выглядеть такое взаимодействие:

src/routes/send-message/+page.svelte
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button onclick={rerun}>Перезапуск CI</button>
src/routes/api/ci/+server.js
/** @type {import('./$types').RequestHandler} */
export function 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> для управления поведением роутера.