Enregistrer les Données
Ajoutons une nouvelle table à notre base de données. Ouvrez api/prisma/schema.prisma
et ajoutez un nouveau modèle "Contact" à la suite du premier modèle "Post":
Syntaxe Prisma pour les champs facultatifs
Pour définir une colonne comme optionnelle (c'est à dire permettre que sa valeur soit
NULL
), il suffit de suffixer le type de la donnée avec un point d'interrogation:name String?
Cela permettraname
d'accepter une valeur de type chaîne de caractères, ouNULL
. Cela permettra à la valeur dename
d'être soit de typestring
, soitNULL
.
Nous créons ensuite notre nouvelle migration:
Enfin, nous executons la migration de façon à mettre à jour le schéma de la base de données:
Maintenant nous créeons l'interface GraphQL permettant d'accéder à cette nouvelle table. C'est la première fois que nous utilisons cette commande generate
nous même. (la commande scaffold
repose également dessus):
De la même manière qu'avec la commande scaffold
, ceci va créer deux nouveaux fichiers dans le répertoire api
:
api/src/graphql/contacts.sdl.js
: qui définit le schéma GraphQLapi/src/services/contacts/contacts.js
: qui contient votre code métier
// api/src/graphql/contacts.sdl.js type Mutation { createContact(input: CreateContactInput!): Contact }
Que sont les "input" CreateContactInput
et UpdateContactInput
? Redwood suit la recommandation de GraphQL d'utiliser les Input Types dans les mutations plutôt que de lister tous les champs qui peuvent être définis. Tous les champs requis dans schema.prisma
sont également requis dans CreateContactInput
(vous ne pouvez pas créer un enregistrement valide sans eux) mais rien n'est explicitement requis dans UpdateContactInput
. En effet, vous pouvez souhaiter mettre à jour un seul champ, deux champs ou tous les champs. L'alternative serait de créer des types d'entrée séparés pour chaque permutation de champs que vous souhaitez mettre à jour. Nous avons estimé que le fait de n'avoir qu'une seule entrée de mise à jour, bien que ce ne soit peut-être pas la manière absolument correcte de créer une API GraphQL, était un bon compromis pour faciliter le développement.
Redwood suppose que votre code n'essaiera pas de définir une valeur sur un champ nommé
id
oucreatedAt
donc il les a laissés en dehors des types d'entrée, mais si votre base de données autorise l'un ou l'autre de ceux à définir manuellement, vous pouvez mettre à jourCreateContactInput
ouUpdateContactInput
et les ajouter.
Puisque toutes les colonnes de la table étaient définies comme requises dans schema.prisma
, elles sont également définies comme requises ici (notez le suffixe !
sur les types de données).
Syntaxe GraphQL pour les champs obligatoires
La syntaxe SDL de GraphQL nécessite un
supplémentaire !
lorsqu'un champ est requis. important: la syntaxe deschema.prisma
requiert l'ajout d'un caractère?
lorsqu'un champ n'est pas requis, tandis que la syntaxe GraphQL requiert l'ajout d'un caractère!
lorsqu'un champ est requis.
Comme décrit dans Quête secondaire: Fonctionnement de Redwood avec les Données, il n'y a pas de "resolver" définit explicitement dans le fichier SDL. Redwood suit une convention de nommage simple: chaque champ listé dans les types Query
et Mutation
correspondent à une fonction avec un nom identique dans les fichiers service
et sdl
associés (api/src/graphql/contacts.sdl.js -> api/src/services/contacts/contacts.js
)
Dans le cas présent, nous créeons une unique Mutation
que nous appelons createContact
. Nous l'ajoutons à la fin de notre fichier SDL (avant le caractère 'backtick'):
La mutation createContact
accepte une variable unique, input
, qui est un objet conforme à ce qu'on attend pour un CreateContactInput
, c'est à dire { name, email, message }
.
C'est terminé pour le fichier SDL, définissons maintenant le service qui va réellement enregistrer les données en base. Le service inclut une fonction contacts
permettant de récupérer l'ensemble des contacts depuis la base. Ajoutons-y une mutation pour pouvoir créer un nouveau contact:
Grâce au client Prisma, il faut peu de code pour enregistrer nos données en base! Il s'agit d'un appel asynchrone, mais nous n'avons pas à nous soucier de manipuler un objet Promise ou s'arranger avec async/await
. La librairie Apollo le fait pour nous!
Avant d'insérer tout ceci dans notre interface utilisateur, prennons un peu de temps pour utiliser un outil bien pratique en exécutant la commande yarn redwood dev
.
#
Le Bac à Sable GraphQLSouvent, il est utile d'expérimenter notre API dans une forme un peu "brute" avant de poursuivre plus avant le développement de l'interface et s'apercevoir que l'on a oublié quelque chose. Y a-t-il une faute de frappe dans la couche API ou la couche web ? Découvrons en accédant uniquement à la couche API.
Lorsque vous avez exécuté la commande yarn redwood dev
au début de ce didacticiel, vous avez en réalité démarré un second processus en arrière-plan. Ouvrez donc une nouvelle page de votre navigateur à cette adresse: http://localhost:8911/graphql . Il s'agit du Bac à Sable GraphQL fournit par la librairie Prisma, une application web permettant d'interagir avec une API GraphQL:
Observez en particulier l'onglet "Doc" situé sur la partie droite de l'écran:
Vous y trouverez le schema complet tel que définit dans vos fichiers SDL! L'application analyse ces définitions et vous propose ces éléments pour vous permettre de construire vos requêtes. Essayez par exemple de récupérer les ID de tous les articles en écrivant votre requête dans la partie gauche puis en cliquant sur le bouton "Play":
Le bac à sable GraphQL est une excellente manière d'expérimenter avec votre API, et comprendre pourquoi une requête ne fonctionne pas comme prévue.
#
Créer un ContactNotre mutation GraphQL est prête pour la partie backend, tout ce qu'il reste à faire c'est l'invoquer depuis la partie frontend. Tout ce qui à trait à notre formulaire se trouve dans ContactPage
, c'est donc l'endroit logique pour y mettre l'appel à notre nouvelle mutation. D'abord nous définissons cette mutation comme une constante que nous appellerons plus tard (ceci peut être défini en dehors du composant lui-même, juste après les lignes d'imports):
Nous référençons ainsi la mutation createContact
définie auparavant dans le fichier SDL des contacts, tout en lui passant en argument un objet input
contenant la valeur des champs name
, email
et message
.
Après quoi, nous appelons le 'hook' useMutation
fourni par Appolo, ce qui nous permet d'exécuter la mutation lorsque le moment est venu (n'oubliez pas les imports comme à chaque fois):
create
est une fonction qui invoque la mutation et prend en paramètre un objet contenant un clef variables
. Par exemple, nous pourrions l'appeler également de cette manière:
Si votre mémoire est bonne, vous vous souvenez sans doute que la balise <Form>
nous donne accès à l'ensemble des champs du formulaire avec un objet bien pratique dans lequel chaque clef se trouve être le nom du champ. Cela signifie donc que l'objet data
que nous recevons dans onSubmit
est déjà dans le format adapté pour input
!
Maintenant nous pouvons mettre à jour la fonction onSubmit
pour invoquer la mutation avec les données qu'elle reçoit:
Essayez-donc de remplir le formulaire et de l'envoyer. Vous devriez obtenir un nouveau contact en base de données! Vous pouvez vérifier ceci avec l'outil bac à sable de GraphQL:
#
Améliorer le formulaire de contactNotre formulaire de contact fonctionne, mais il subsiste quelques problèmes:
- Cliquer sur le bouton d'enregistrement plusieurs fois à pour conséquence d'envoyer le formulaire également plusieurs fois
- L'utilisateur ne sait pas si l'envoi a bien été pris en compte
- Si une erreur devait se produire côté serveur, nous n'avons aucun moyen d'en informer l'utilisateur
Essayons d'y apporter une solution.
Le 'hook' useMutation
retourne quelques autres éléments en plus de la fonction permettant de l'invoquer. Nous pouvons les détruire comme le deuxième élément du tableau qui est retourné. Les deux choses qui nous intéressent sont le chargement
et erreur
:
Ce faisant, nous savons si un appel à la base est toujours en cours en utilisant la valeur de loading
. Une façon simple de résoudre le problème des soumissions multiples du même formulaire est de rendre inactif le bouton d'envoi tant que la réponse n'a pas été reçue. Nous pouvons faire celà en liant l'attribut disabled
du bouton "save" à la valeur contanue dans loading
:
Il peut être difficile de voir une différence en phase de développement car l'envoi est très rapide. Mais vous pouvez néanmoins activer un outil bien pratique dans le navigateur Chrome afin de simuler une connection lente:
Vous verrez alors que le bouton "Save" devient inactif pendant une seconde ou deux en attendant la réponse.
Ensuite, affichons une notification pour faire savoir à l'utilisateur que son envoi a réussi. Redwood inclut react-hot-toast pour afficher rapidement une notification sur une page.
useMutation
accepte un second paramètre optionnel contenant des options. Une de ces options est une fonction callback appelée onCompleted
qui sera invoquée lorsque la mutation sera achevée avec succès. Nous allons utiliser ce callback pour appeler une fonction toast()
qui ajoutera un message à afficher dans un composant <Toaster>.
Ajoutez le callback onCompleted
à useMutation
et incluez le composant <Toaster> dans notre return
, juste avant le <Form>. Nous avons également besoin de tout envelopper dans un fragment (<></>
) parce que nous ne sommes autorisés à renvoyer qu'un seul élément :
Vous pouvez lire la documentation complète pour Toast ici.
#
Afficher les erreurs serveurNous allons maintenant informer l'utilisateur des éventuelles erreurs côté serveur. Jusqu'ici nous n'avons notifié les utilisateurs quie des erreurs côté client lorsqu'un champ était manquant ou formaté incorrectement. Mais si nous avons également des contraintes côté serveur que le composant <Form>
ignore, nous devons tout de même pouvoir en informer l'utilisateur.
Ainsi, nous avons une validateur de l'email côté client, mais tout bon développeur web sait qu'il ne faut jamais faire confiance au client. Ajoutons une validation de l'email côté serveur de façon à être certain qu'aucune donnée erronée ne soit ajoutée dans la base, et ce même si un utilisateur parvenait à contourner le fonctionnement de l'application côté client.
Pas de validation côté serveur ?
Pourquoi n'avons-nous pas besoin de validation côté serveur pour s'assurer que les champs name, email et message sont bien remplis? Car la base de données le fait pour nous. Vous rappellez-vous
String!
dans notre fichier SDL? Celà ajoute une contrainte en base de données de telle façon que ce champ ne puisse êtrenull
. Une valeurnull
serait rejetée par la base et GraphQL renverrait une erreur à la partie client.Cependant, il n'existe pas de type
Email!
, raison pour laquelle nous devons assurer la validation nous même
Nous avons déjà évoqué le fait que la logique métier de notre application se trouve dans nos services, et il s'agit ici d'un parfait exemple. Ajoutons une fonction validate
à notre service contacts
:
Ainsi, lorsque createContact
est invoquée, la fonction commence par valider le contenu des champs du formulaire. Puis, et seulement s'il n'y a aucune erreur, l'enregistrement sera créé en base de données.
Nous capturons déjà toutes les erreurs dans la constante error
que nous obtenons grâce au 'hook' useMutation
. C'est pourquoi nous avons la possibilité d'afficher ces erreurs sur la page, par exemple au dessus du formulaire:
Si vous avez besoin de manipuler l'objet contenant les erreurs, vous pouvez procéder ainsi:
Pour obtenir une erreur de serveur à exécuter, nous allons supprimer la validation du format de courriel afin que l'erreur côté client ne soit pas affichée :
Maintenant, essayez de remplir le formulaire avec une adresse e-mail invalide :
Ce n'est pas joli, mais ça marche. Ce serait sans doute mieux si le champ lui-même était mis en surbrillance comme quand la validation en ligne était en place...
Rappelez-vous quand nous avons dit que <Form>
avait encore un tour dans sa manche ? Voilà qui vient !
Supprimez l'affichage de l'erreur tel que nous venons de l'ajouter ({ error && ...}
) , et remplacez-le avec <FormError>
tout en passant en argument la constante error
que nous récupérons depuis useMutation
. Ajoutez également quelques éléments de style à wrapperStyle
, sans oublier les import
associés. Nous passerons également erreur
à <Form>
pour qu'il puisse configurer un contexte :
Maintenant, soumettez un message avec une adresse e-mail invalide :
Nous obtenons ce message d'erreur en haut en disant que quelque chose s'est mal passé en anglais clair et le champ réel est mis en surbrillance pour nous, tout comme la validation en ligne ! Le message en haut du formulaire peut apparaître un peu lourd pour un si petit formulaire, mais vous contaterez son utilité lorsque vous construirez des formulaires de plusieurs pages; de cette façon l'utilisateur peut voir imméédiatement ce qui ne fonctionne pas sans avoir à parcourir l'ensemble du formulaire. Si vous ne souhaitez pas utiliser cet affichage, il vous suffit de supprimer <FormError>
, les champs seront toujours mis en avant.
<FormError>
a plusieurs options pour adapter le style d'affichage
wrapperStyle
/wrapperClassName
: le conteneur pour l'ensemble du messagetitleStyle
/titleClassName
: le titre "Can't create new contact"listStyle
/listClassName
: le<ul>
qui contient la liste des erreurslistItemStyle
/listItemClassName
: chaque<li>
contenant chaque erreur
#
Une dernière chose...Puisque nous ne redirigeons pas l'utilisateur une fois le formulaire envoyé, nous devrions au moins remettre le formulaire à zéro. Pour celà nous devons utiliser la fonction reset()
proposée par react-hook-form
, mais nous n'y avons pas accès compte tenu de la manière dont nous utilisons <Form>
.
react-hook-form
possède un 'hook' appelé useForm()
qui est en principe invoquée pour nous à l'intérieur de <Form>
. De façon à réinitialiser le formulaire nous devons invoquer ce 'hook' manuellement. Mais la fonctionnalité que useForm()
fournit doit tout de même être utilisée dans Form
. Voici comment nous y parvenons.
Tout d'abord, nous allons importer useForm
:
Puis invoquons ce 'hook' dans notre composant:
Enfin, nous allons dire à <Form>
d'utiliser les formMethods
que nous venons d'instancier au lieu de le faire lui-même :
Maintenant nous pouvons appeler reset()
sur formMethods
après avoir appelé toast()
:
Vous pouvez maintenant réactiver la validation email côté client sur le
<TextField>
, tout en conservant la validation côté serveur.
Voici le contenu final de la page ContactPage.js
:
C'est tout! [React Hook Form](https://react-hook-form.com/) propose pas mal de fonctionalités que <Form>
n'expose pas. Lorsque vous souhaitez les utiliser, appelez juste le 'hook' useForm()
vous-même, en vous assurant de bien passer en argument l'objet retourné (formMethods
) comme propriété de <Form>
de façon à ce que la validation et les autres fonctionalités puissent continuer à fonctionner.
Vous avez peut-être remarqué que la validation onBlur a cessé de fonctionner lorsque vous avez commencé à appeler
userForm()
par vous-même. Ceci s'explique car Redwood invoqueuserForm()
et lui passe automatiquement en argument ce que vous avez passé à<Form>
. Puisque Redwood n'appelle plus automatiquementuseForm()
à votre place, vous devez de faire manuellement:
La partie publique du site a bon aspect. Que faire maintenant de la partie administration qui nous permet de créer et éditer les articles? Nous devrions la déplacer dans une partie réservée et la placer derrière un login, de façon à ce des utilisateurs mal intentionnés ne puissent pas créer en chaîne, par exemple, des publicités pour l'achat de médicaments en ligne...