Skip to main content

Actions de formulaire

Un fichier +page.server.js peut exporter des actions, ce qui vous permet d’envoyer des données avec POST vers le serveur en utilisant un élément de formulaire <form>.

Lorsque vous utilisez un élément <form>, la présence de JavaScript côté client est optionnelle, mais vous pouvez facilement améliorer progressivement vos interactions de formulaire avec JavaScript pour fournir la meilleure expérience utilisateur.

La fonction distante expérimentale form couvre les mêmes cas d’usage que les actions de formulaire, en y ajoutant du typage et des mutations single-flight. Les actions de formulaire sont polyvalentes et vont continuer de fonctionner, mais les nouveaux développements sont focalisés sur les fonctions distantes, qui sont conçues pour devenir la méthode recommandée de communiquer avec le serveur. Vous pouvez envisager l’usage des fonctions distantes pour vos nouveaux projets, en gardant à l’esprit que l’API peut changer tant que cette fonctionnalité est expérimentale.

Actions par défaut

Dans le cas le plus simple, une page déclare une action par défaut appelée default :

src/routes/login/+page.server
/** @satisfies {import('./$types').Actions} */
export const actions = {
	default: async (event) => {
		// TODO identifier l'utilisateur
	}
};
import type { Actions } from './$types';

export const actions = {
	default: async (event) => {
		// TODO identifier l'utilisateur
	}
} satisfies Actions;

Pour invoquer cette action depuis la page /login, ajouter simplement un élément <form> — aucun JavaScript n’est nécessaire :

src/routes/login/+page
<form method="POST">
	<label>
		Email
		<input name="email" type="email">
	</label>
	<label>
		Mot de passe
		<input name="password" type="password">
	</label>
	<button>Se connecter</button>
</form>

Lorsque le bouton est cliqué, le navigateur va envoyer au serveur les données du formulaire via une requête POST, en exécutant l’action par défaut.

Les actions utilisent toujours des requêtes POST, puisque les requêtes GET ne sont pas censées avoir d’effets de bord.

Nous pouvons aussi invoquer l’action depuis d’autres pages (par exemple, s’il y a un widget de connexion dans la barre de navigation du layout racine) en ajoutant l’attribut action pointant vers la page :

src/routes/+layout
<form method="POST" action="/login">
	<!-- contenu -->
</form>

Actions nommées

Plutôt qu’une seule action default par défaut, une page peut avoir autant d’actions nommées que nécessaire :

src/routes/login/+page.server
/** @satisfies {import('./$types').Actions} */
export const actions = {
	default: async (event) => {
	login: async (event) => {
		// TODO connecter l'utilisateur
	},
	register: async (event) => {
		// TODO inscrire l'utilisateur
	}
};
import type { Actions } from './$types';

export const actions = {
	default: async (event) => {
	login: async (event) => {
		// TODO connecter l'utilisateur
	},
	register: async (event) => {
		// TODO inscrire l'utilisateur
	}
} satisfies Actions;

Pour invoquer une action nommée, ajoutez un paramètre de requête comportant le nom de l’action préfixé par un caractère / :

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

Similaire à l’attribut action, nous pouvons utiliser l’attribut formaction sur un bouton pour envoyer avec POST les mêmes données de formulaire à une action différente de celle de l’élément <form> parent :

src/routes/login/+page
<form method="POST" action="?/login">
	<label>
		Email
		<input name="email" type="email">
	</label>
	<label>
		Mot de passe
		<input name="password" type="password">
	</label>
	<button>Se connecter</button>
	<button formaction="?/register">S'inscrire</button>
</form>

Il n’est pas possible d’avoir des actions par défaut dans le même fichier que des actions nommées, car si vous ciblez une action nommée avec POST sans redirection, le paramètre de requête est persisté dans l’URL, ce qui signifie que la prochaine requête POST par défaut va cibler l’action nommée précédente.

Anatomie d’une action

Chaque action reçoit un objet RequestEvent, vous permettant de lire les données avec request.formData(). Après le traitement de la requête (par exemple, pour connecter l’utilisateur ou l’utilisatrice en définissant un cookie), l’action peut répondre avec des données qui seront disponibles via la propriété form sur la page correspondante, et via page.form dans toute l’application, jusqu’à sa prochaine mise à jour.

src/routes/login/+page.server
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 inscrire l'utilisateur
	}
};
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 inscrire l'utilisateur
	}
} satisfies Actions;
src/routes/login/+page
<script>
	/** @type {import('./$types').PageProps} */
	let { data, form } = $props();
</script>

{#if form?.success}
	<!-- ce message est éphémère ; il existe car la page a été rendu en réponse à une soumission
				de formulaire. il disparaîtra si l'utilisateur recharge la page -->
	<p>Connexion réussie ! Content de vous revoir, {data.user.name}</p>
{/if}
<script lang="ts">
	import type { PageProps } from './$types';

	let { data, form }: PageProps = $props();
</script>

{#if form?.success}
	<!-- ce message est éphémère ; il existe car la page a été rendu en réponse à une soumission
				de formulaire. il disparaîtra si l'utilisateur recharge la page -->
	<p>Connexion réussie ! Content de vous revoir, {data.user.name}</p>
{/if}
Legacy mode

PageProps a été ajouté dans la version 2.16.0 de SvelteKit. Dans les versions antérieures, vous deviez typer les propriétés data et form individuellement :

+page
/** @type {{ data: import('./$types').PageData, form: import('./$types').ActionData }} */
let { data, form } = $props();
import type { PageData, ActionData } from './$types';

let { data, form }: { data: PageData, form: ActionData } = $props();

Avec Svelte 4, vous utiliseriez plutôt export let data et export let form pour déclarer les propriétés.

Erreurs de validation

Si la requête n’a pas pu être traitée à cause de données non valides, vous pouvez renvoyer des erreurs de validation — en plus des valeurs précédemment soumises par le formulaire — à l’utilisateur ou l’utilisatrice afin qu’il ou elle ré-essaye. La fonction fail vous permet de renvoyer un statut HTTP (généralement 400 ou 422, dans le cas d’erreurs de validation) avec les données. Le statut est rendu disponible via page.status et les données via form :

src/routes/login/+page.server
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 inscrire l'utilisateur
	}
};
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 inscrire l'utilisateur
	}
} satisfies Actions;

Notez que par précaution, nous renvoyons uniquement l’email à la page — pas le mot de passe.

src/routes/login/+page
<form method="POST" action="?/login">
	{#if form?.missing}<p class="error">Le champ email est requis</p>{/if}
	{#if form?.incorrect}<p class="error">Identifiants invalides !</p>{/if}
	<label>
		Email
		<input name="email" type="email" value={form?.email ?? ''}>
	</label>
	<label>
		Mot de passe
		<input name="password" type="password">
	</label>
	<button>Se connecter</button>
	<button formaction="?/register">S'inscrire</button>
</form>

Les données renvoyées doivent être sérialisables en JSON. À part ça, la structure est complètement libre. Par exemple, si vous avez plusieurs formulaires sur la même page, vous pourriez différencier à quel élément <form> les données form font référence avec une propriété id ou similaire.

Redirections

Les redirections (et les erreurs) fonctionnent exactement de la même façon que dans load :

src/routes/login/+page.server
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 inscrire l'utilisateur
	}
};
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 inscrire l'utilisateur
	}
} satisfies Actions;

Chargement de données

Après l’exécution d’une action, la page sera re-rendue (à moins qu’une redirection ou une erreur inattendue se produise), avec la valeur de retour de l’action rendue disponible dans la page en tant que propriété form. Ceci signifie que les fonctions load de la page vont être exécutées après la fin de l’exécution d’une action.

Notez que handle est exécuté avant l’invocation de l’action, et n’est pas ré-exécutée avant les fonctions load. Ceci signifie que si, par exemple, vous utiliser handle pour remplir event.locals en fonction d’un cookie, vous devez mettre à jour event.locals lorsque vous définissez ou supprimez le cookie lors d’une action :

src/hooks.server
/** @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);
};
src/routes/account/+page.server
/** @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;

Amélioration progressive

Dans les sections précédentes, nous avons construit une action /login qui fonctionne sans JavaScript côté client — aucun fetch n’a été impliqué. C’est super, mais lorsque JavaScript est disponible, nous pouvons améliorer progressivement nos interactions de formulaire pour fournir une meilleure expérience utilisateur.

use:enhance

La manière la plus simple d’améliorer un formulaire progressivement est d’ajouter l’action use:action :

src/routes/login/+page
<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 peut uniquement être utilisé avec des formulaires qui ont method="POST" et pointent vers des actions définies dans un fichier +page.server.js. Cela ne fonctionnera pas avec method="GET", qui est le fonctionnement par défaut pour des formulaires sans méthode précisée. Utiliser use:enhance sur des formulaires sans method="POST" ou cibler un endpoint +server.js va provoquer une erreur.

Oui, il peut y avoir de la confusion entre l’action enhance et <form action>, qui sont toutes les deux appelées “action”. Cette documentation est pleine d’actions. Désolé.

Sans argument, use:enhance va émuler le comportement natif du navigateur, simplement sans le rechargement complet de la page. Plus précisément, cela va :

  • mettre à jour la propriété form, ainsi que page.form et page.status lors d’une réponse réussie ou invalide, mais uniquement si l’action est sur la même page que celle depuis laquelle vous soumettez. Par exemple, si vous formulaire ressemble à <form action="/somewhere/else" ..>, la propriété form et l’état page.form ne seront pas mis à jour. En effet, la soumission native d’un formulaire vous redirige vers la page sur laquelle l’action est définie. Si vous souhaitez tout de même les mettre à jour, vous devez utiliser applyAction
  • réinitialiser l’élément <form>
  • invalider toutes les données avec invalidateAll si la réponse est réussie
  • appeler goto si la réponse est une redirection
  • afficher la frontière d’erreur +error la plus proche si une erreur se produit
  • réinitialiser le focus sur l’élément approprié

Personnaliser use:enhance

Pour personnaliser le comportement, vous pouvez fournir une SubmitFunction qui sera exécutée immédiatement avant la soumission du formulaire, et (optionnellement) renvoyer un callback qui fournira une ActionResult.

<form
	method="POST"
	use:enhance={({ formElement, formData, action, cancel, submitter }) => {
		// `formElement` est l'élément `<form>`
		// `formData` est son objet `FormData` qui est sur le point d'être soumis
		// `action` est l'URL vers laquelle le formulaire pointe
		// exécuter `cancel()` va empêcher la soumission
		// `submitter` est l'élément `HTMLElement` qui a provoqué la soumission du formulaire

		return async ({ result, update }) => {
			// `result` est un objet `ActionResult`
			// `update` est une fonction qui déclenche la logique qui aurait été exécutée par défaut si ce
			// callback n'avait pas été défini
		};
	}}
>

Vous pouvez utiliser ces fonctions pour afficher et cacher des éléments d’interface de chargement, entre autres.

Si vous renvoyez un callback, vous écrasez le comportement post-soumission par défaut. Pour le récupérer, appelez update, qui accepte les paramètres invalidateAll et reset, ou utilisez applyAction sur le résultat :

src/routes/login/+page
<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` est un objet `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` est un objet `ActionResult`
			if (result.type === 'redirect') {
				goto(result.location);
			} else {
				await applyAction(result);
			}
		};
	}}
>

Le comportement de applyAction(result) dépend de result.type :

  • success, failure — définit page.status en tant que result.status et met à jour form et page.form en tant que result.data (indépendamment de la source de soumission, à la différence du update venant de enhance)
  • redirect — exécute goto(result.location, { invalidateAll: true })
  • error — affiche la frontière +error la plus proche en utilisant result.error

Dans tous les cas, le focus sera réinitialisé.

Gestionnaire personnalisé d’évènement [VO]Custom event listener

Nous pouvons également implémenter de l’amélioration progressive nous-même, sans use:enhance, avec un gestionnaire d’évènement normal sur l’élément <form> :

src/routes/login/+page
<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, event.submitter);

		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') {
			// ré-exécute toutes les fonctions `load`, en cas de succès
			await invalidateAll();
		}

		applyAction(result);
	}
</script>

<form method="POST" onsubmit={handleSubmit}>
	<!-- contenu -->
</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, event.submitter);

		const response = await fetch(event.currentTarget.action, {
			method: 'POST',
			body: data
		});

		const result: ActionResult = deserialize(await response.text());

		if (result.type === 'success') {
			// ré-exécute toutes les fonctions `load`, en cas de succès
			await invalidateAll();
		}

		applyAction(result);
	}
</script>

<form method="POST" onsubmit={handleSubmit}>
	<!-- contenu -->
</form>

Notez que vous avez besoin de deserialize la réponse avant de la traiter en utilisant la méthode correspondante importée depuis $app/forms. JSON.parse() ne suffit pas car les actions de formulaires — comme les fonctions load — supportent également le renvoi d’objets Date ou BigInt.

Si vous avez un fichier +server.js associé à votre +page.server.js, les requêtes fetch vont y être dirigées par défaut. Si vous souhaitez envoyer une requête POST à une action de +page.server.js, utilisez l’en-tête personnalisée x-sveltekit-action :

const response = await fetch(this.action, {
	method: 'POST',
	body: data,
	headers: {
		'x-sveltekit-action': 'true'
	}
});

Alternatives

Les actions de formulaire sont la méthode recommandée pour envoyer des données au serveur, puisqu’elles peuvent être améliorées progressivement, mais vous pouvez également utiliser des fichiers +server.js pour exposer (par exemple) une API JSON. Voici ce à quoi une telle interaction pourrait ressembler :

src/routes/send-message/+page
<script>
	function rerun() {
		fetch('/api/ci', {
			method: 'POST'
		});
	}
</script>

<button onclick={rerun}>Relancer la CI</button>
<script lang="ts">
	function rerun() {
		fetch('/api/ci', {
			method: 'POST'
		});
	}
</script>

<button onclick={rerun}>Relancer la CI</button>
src/routes/api/ci/+server
/** @type {import('./$types').RequestHandler} */
export function POST() {
	// faire quelque chose
}
import type { RequestHandler } from './$types';
export const POST: RequestHandler = () => {
	// faire quelque chose
};

GET vs POST

Comme nous venons de le voir, pour invoquer une action de formulaire, vous devez utiliser method="POST".

Certains formulaires n’ont pas besoin d’utiliser POST pour envoyer des données au serveur — les inputs de recherche par exemple. Pour ces cas-là, vous pouvez utiliser method="GET" (ou de manière équivalente, aucune method), et SvelteKit va traiter ces requêtes comme si elles venaient d’éléments <a>, utilisant le routeur client plutôt qu’une navigation impliquant le rechargement complet de la page :

<form action="/search">
	<label>
		Rechercher
		<input name="q">
	</label>
</form>

Soumettre ce formulaire va déclencher une navigation vers /search?q=... et invoquer votre fonction load, mais ne va invoquer aucune action. Comme pour les éléments <a>, vous pouvez définir les attributs data-sveltekit-reload, data-sveltekit-replacestate, data-sveltekit-keepfocus et data-sveltekit-noscroll sur l’élément <form> pour contrôler le comportement du routeur.

Sur le même sujet

Modifier cette page sur Github llms.txt