Concepts principaux
Actions de formulaire
Éditer cette page sur GithubUn fichier +page.server.js
peut exporter des actions, qui vous permettent d'envoyer avec POST
des données au serveur en utilisant l'élément <form>
.
Lorsque vous utilisez un formulaire natif <form>
, l'envoi du formulaire est réalisé sans JavaScript, rendant JavaScript complètement facultatif sur la page, mais vous pouvez améliorer progressivement les interactions du formulaire de manière simple avec JavaScript pour proposer une meilleure expérience utilisateur (plus d'infos dans cette section).
Actions par défautpermalink
Dans le cas le plus simple, une page déclare une action default
:
ts
/** @type {import('./$types').Actions} */export constactions = {default : async (event ) => {// TODO connecter l'utilisateur}};
ts
import type {Actions } from './$types';export constactions = {default : async (event ) => {// TODO connecter l'utilisateur},} satisfiesActions ;
Pour invoquer cette action depuis la page /login
, ajoutez simplement un <form>
— vous n'avez pas besoin de JavaScript :
<form method="POST">
<label>
Email
<input name="email" type="email">
</label>
<label>
Mot de passe
<input name="password" type="password">
</label>
<button>Connexion</button>
</form>
Si quelqu'un clique sur le bouton, le navigateur enverra au serveur la donnée du formulaire via une requête POST
, déclenchant l'action par défaut.
Les actions utilisent toujours des requêtes
POST
, puisque les requêtesGET
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 bouton de connexion dans la barre de navigation du layout racine) en ajoutant l'attribut action
qui pointe vers la page :
<form method="POST" action="/login">
<!-- contenu -->
</form>
Actions nomméespermalink
À la place d'une action default
, une page peut avoir autant d'actions nommées que nécessaire :
/** @type {import('./$types').Actions} */
export const actions = {
default: async (event) => {
login: async (event) => {
// TODO connecter l'utilisateur
},
register: async (event) => {
// TODO inscrire l'utilisateur
}
};
Pour invoquer une action nommée, ajouter un paramètre de requête dont le nom est préfixé par un /
:
<form method="POST" action="?/register">
<form method="POST" action="/login?/register">
Comme pour l'attribut action
, nous pouvons utiliser l'attribut formaction
sur le bouton pour envoyer avec POST
la même donnée de formulaire à une action différente de celle du <form>
originel :
<form method="POST">
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email">
</label>
<label>
Mot de passe
<input name="password" type="password">
</label>
<button>Connexion</button>
<button formaction="?/register">Inscription</button>
</form>
Nous ne pouvons pas avoir une action par défaut en même temps que des actions nommées, car si vous envoyez avec
POST
à une action nommée sans redirection, le paramètre de recherche est persisté dans l'URL, ce qui signifie que la prochaine requêtePOST
par défaut repasserait par la même action nommée que précédemment.
Anatomie d'une actionpermalink
Chaque action reçoit un objet RequestEvent
, vous permettant de lire la donnée avec request.formData()
. Après avoir traité la requête (par exemple, en connectant l'utilisateur grâce à un cookie), l'action peut répondre avec des données qui seront disponibles au travers de la propriété form
dans la page correspondante, et à travers $page.form
dans toute l'application jusqu'à la prochaine mise à jour.
ts
/** @type {import('./$types').PageServerLoad} */export async functionCannot find name 'db'.2304Cannot find name 'db'.load ({cookies }) {constCannot find name 'db'.2304Cannot find name 'db'.user = awaitdb .getUserFromSession ( cookies .get ('sessionid'));return {user };}/** @type {import('./$types').Actions} */export constactions = {login : async ({cookies ,request }) => {constdata = awaitrequest .formData ();constdata .get ('email');constpassword =data .get ('password');constuser = awaitdb .getUser (cookies .set ('sessionid', awaitdb .createSession (user ), {path : '/' });return {success : true };},register : async (event ) => {// TODO inscrire l'utilisateur}};
ts
import type {PageServerLoad ,Actions } from './$types';Cannot find name 'db'.2304Cannot find name 'db'.export constCannot find name 'db'.2304Cannot find name 'db'.load :PageServerLoad = async ({cookies }) => {constuser = awaitdb .getUserFromSession (cookies .get ('sessionid'));return {user };};export constactions = {login : async ({cookies ,request }) => {constdata = awaitrequest .formData ();constdata .get ('email');constpassword =data .get ('password');constuser = awaitdb .getUser (cookies .set ('sessionid', awaitdb .createSession (user ), {path : '/' });return {success : true };},register : async (event ) => {// TODO inscrire l'utilisateur},} satisfiesActions ;
<script>
/** @type {import('./$types').PageData} */
export let data;
/** @type {import('./$types').ActionData} */
export let form;
</script>
{#if form?.success}
<!-- ce message est ephémère ; il existe parce que la page a été rendue en
réponse à la soumission du formulaire. il disparaîtra si l'utilisateur recharge la page -->
<p>Vous êtes bien connecté•e ! Ravi de vous revoir, {data.user.name}</p>
{/if}
<script lang="ts">
import type { PageData, ActionData } from './$types';
export let data: PageData;
export let form: ActionData;
</script>
{#if form?.success}
<!-- ce message est ephémère ; il existe parce que la page a été rendue en
réponse à la soumission du formulaire. il disparaîtra si l'utilisateur recharge la page -->
<p>Vous êtes bien connecté•e ! Ravi de vous revoir, {data.user.name}</p>
{/if}
Erreurs de validationpermalink
Si la requête n'a pas pu être traitée à cause de données invalides, vous pouvez renvoyer des erreurs de validation — en plus des valeurs du formulaire reçues — à l'utilisateur ou l'utilisatrice pour qu'elle réessaie. La fonction fail
vous permet de renvoyer un code HTTP (en général 400 ou 422, dans le cas d'erreurs de validation) avec la donnée. Le code est disponible via $page.status
et la donnée via form
:
import { fail } from '@sveltejs/kit';
/** @type {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 !== 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
}
};
Notez que par précaution, nous renvoyons uniquement l'email à la page — pas le mot de passe.
<form method="POST" action="?/login">
{#if form?.missing}<p class="error">The email field is required</p>{/if}
{#if form?.incorrect}<p class="error">Invalid credentials!</p>{/if}
<label>
Email
<input name="email" type="email">
<input name="email" type="email" value={form?.email ?? ''}>
</label>
<label>
Mot de passe
<input name="password" type="password">
</label>
<button>Connexion</button>
<button formaction="?/register">Inscription</button>
</form>
La donnée renvoyée doit être sérialisable en JSON. À part ça, vous pouvez utiliser la structure que vous voulez. Par exemple, si vous avez plusieurs formulaires sur la page, vous pouvez distinguer à quel <form>
la donnée form
fait référence avec une propriété id
ou équivalent.
Redirectionspermalink
Les redirections (et erreurs) fonctionnent exactement de la même façon que dans load
:
import { fail, redirect } from '@sveltejs/kit';
/** @type {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 !== 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
}
};
Chargement de donnéespermalink
Après l'exécution d'une action, la page est re-rendue (à moins qu'une redirection ou une erreur inattendue ne se produise). La valeur de retour de l'action rendue disponible dans la page en tant que la prop form
. Cela implique que les fonctions load
de votre page sont exécutées après l'exécution de l'action.
Notez que handle
est exécutée avant l'invocation de l'action, et n'est pas rejouée avant les fonctions load
. Cela signifie que si, par exemple, vous utilisez handle
pour remplir event.locals
en fonction d'un cookie, vous devez mettre à jour event.locals
lorsque vous définissez ou supprimez le cookie dans une action :
ts
/** @type {import('@sveltejs/kit').Handle} */export async functionhandle ({event ,resolve }) {event .locals .user = awaitgetUser (event .cookies .get ('sessionid'));returnresolve (event );}
ts
import type {Handle } from '@sveltejs/kit';export consthandle :Handle = async ({event ,resolve }) => {event .locals .user = awaitgetUser (event .cookies .get ('sessionid'));returnresolve (event );};
ts
/** @type {import('./$types').PageServerLoad} */export functionload (event ) {return {user :event .locals .user };}/** @type {import('./$types').Actions} */export constactions = {logout : async (event ) => {event .cookies .delete ('sessionid', {path : '/' });event .locals .user = null;}};
ts
import type {PageServerLoad ,Actions } from './$types';export constload :PageServerLoad = (event ) => {return {user :event .locals .user ,};};export constactions = {logout : async (event ) => {event .cookies .delete ('sessionid', {path : '/' });event .locals .user = null;},} satisfiesActions ;
Amélioration progressivepermalink
Dans les sections précédentes nous avons construit une action /login
qui fonctionne même sans JavaScript côté client — pas un seul fetch
en vue. C'est très bien, mais lorsque JavaScript est disponible, nous pouvons améliorer nos interactions de formulaire pour proposer une meilleure expérience utilisateur.
use:enhancepermalink
La façon la plus simple d'améliorer progressivement un formulaire est d'ajouter l'action use:enhance
:
<script>
import { enhance } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
</script>
<form method="POST" use:enhance>
Oui, c'est un peu déroutant que l'action
enhance
et l'action de formulaire<form action>
soient toutes les deux appelées des "actions". Cette documentation est remplie d'actions. Désolé.
Sans argument, use:enhance
va simuler le comportement natif du navigateur, sauf le chargement intégral de la page. L'action va donc :
- mettre à jour la propriété
form
, les valeurs$page.form
et$page.status
lors d'une réponse valide ou invalide, mais seulement si l'action est sur la même page depuis laquelle vous avez envoyé le formulaire. Par exemple, si votre formulaire ressemble à<form action="/quelque/part/ailleurs" ..>
,form
et$page
ne seront pas mises à jour. Cela s'explique par le fait que lors d'une soumission de formulaire native nous serions redirigés vers la page correspondant à l'action. Si vous souhaitez tout de même les mettre à jour, utilisezapplyAction
- réinitialiser l'élément
<form>
- invalider toutes les données en utilisant
invalidateAll
si la réponse est un succès - appeler
goto
lors d'une réponse de redirection - rendre le composant
+error
le plus proche si une erreur se produit - réinitialiser le focus sur l'élément approprié
Personnaliser use:enhancepermalink
Pour personnaliser le comportement, vous pouvez fournir une fonction de type SubmitFunction
qui sera jouée immédiatement avant la soumission du formulaire, et renverra (optionnellement) un callback qui s'exécutera avec le résultat de l'action ActionResult
. Notez que si vous renvoyez un callback, le comportement par défaut mentionné plus haut ne s'applique pas. Pour qu'il s'applique tout de même dans ce cas, pensez à appeler update
.
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
// `formElement` est l'élément `<form>` courant
// `formData` est l'objet de données `FormData` qui s'apprête à être envoyé
// `action` est l'URL que le formulaire cible
// appeler `cancel()` va empêcher la soumission
// `submitter` est l'élément `HTMLElement` qui a causé la soumission du formulaire
return async ({ result, update }) => {
// `result` est un objet `ActionResult`
// `update` est la fonction qui déclenche la logique par défaut qui serait jouée si le callback n'était pas défini
};
}}
>
Vous pouvez utiliser ces fonctions pour afficher ou cacher une interface de charger, ou autre.
Si vous renvoyez un callback, vous pourriez avoir besoin de reproduire en partie le comportement par défaut de use:enhance
, mais sans invalider les données d'une réponse de succès. Vous pouvez faire cela avec applyAction
:
<script>
import { enhance, applyAction } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
</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
àresult.status
et met à jourform
et$page.form
àresult.data
(peu importe d'où vous soumettez le formulaire, à la différence deupdate
deenhance
)redirect
— appellegoto(result.location, { invalidateAll: true })
error
— rend le composant+error
le plus proche avecresult.error
Dans tous les cas, le focus sera réinitialisé.
Gestionnaire d'évènement personnalisépermalink
Nous pouvons aussi implémenter de l'amélioration progressive nous-même, sans use:enhance
, avec un gestionnaire d'évènement sur l'élément <form>
:
<script>
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
/** @type {import('./$types').ActionData} */
export let form;
/** @type {any} */
let error;
/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
async function handleSubmit(event) {
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') {
// rejoue toutes les fonctions `load`, en cas de soumission réussie
await invalidateAll();
}
applyAction(result);
}
</script>
<form method="POST" on:submit|preventDefault={handleSubmit}>
<!-- contenu -->
</form>
<script lang="ts">
import { invalidateAll, goto } from '$app/navigation';
import { applyAction, deserialize } from '$app/forms';
import type { ActionData } from './$types';
import type { ActionResult } from '@sveltejs/kit';
export let form: ActionData;
let error: any;
async function handleSubmit(event: { currentTarget: EventTarget & HTMLFormElement }) {
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') {
// rejoue toutes les fonctions `load`, en cas de soumission réussie
await invalidateAll();
}
applyAction(result);
}
</script>
<form method="POST" on:submit|preventDefault={handleSubmit}>
<!-- contenu -->
</form>
Notez que vous avez besoin de désérialiser la réponse avant d'effectuer d'autres traitements en utilisant la méthode deserialize
de $app/forms
. JSON.parse()
ne suffit pas car les actions de formulaire – comme les fonctions load
– peuvent aussi renvoyer des objets Date
ou BigInt
.
Si vous avez un fichier +server.js
en plus de votre +page.server.js
, les requêtes fetch
seront envoyées vers +server.js
par défaut. Pour plutôt soumettre avec POST
vers une action de +page.server.js
, utilisez le header personnalisé x-sveltekit-action
:
const response = await fetch(this.action, {
method: 'POST',
body: data,
headers: {
'x-sveltekit-action': 'true'
}
});
Alternativespermalink
Les actions de formulaires sont la méthode à privilégier pour envoyer des données au serveur, puisqu'elles peuvent améliorer progressivement votre application, mais vous pouvez aussi utiliser des fichiers +server.js
pour exposer (par exemple) une API JSON. Voici comment une telle interaction serait écrite :
<script>
function rerun() {
fetch('/api/ci', {
method: 'POST'
});
}
</script>
<button on:click={rerun}>Rerun CI</button>
<script lang="ts">
function rerun() {
fetch('/api/ci', {
method: 'POST',
});
}
</script>
<button on:click={rerun}>Rerun CI</button>
ts
/** @type {import('./$types').RequestHandler} */export functionPOST () {// faire quelque chose}
ts
import type {RequestHandler } from './$types';export constPOST :RequestHandler = () => {// faire quelque chose};
GET vs POSTpermalink
Comme nous l'avons vu, 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 <input>
de recherche par exemple. Dans ces cas-là, vous pouvez utiliser method="GET"
(ou de manière équivalente, ne pas spécifier l'attribut method
), et SvelteKit les traitera alors comme les éléments <a>
, utilisant le routeur client plutôt qu'une navigation rechargeant la page entièrement :
<form action="/search">
<label>
Rechercher
<input name="q">
</label>
</form>
Soumettre ce formulaire va naviguer vers /search?q=...
et invoquer votre fonction load
mais n'invoquera pas d'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 le <form>
pour contrôler le comportement du routeur.