Commentaires
Gérer des formulaires est commun en tant que développeur frontend. Réagir à ce que tape l'utilisateur pour lui indiquer des messages d'erreur ou de succès (ce qui n'empêche pas une vérification côté back, j'insiste), envoyer les données à une API...
Il est donc normal que Vue.JS intègre une façon de gérer les formulaires et plus particulièrement qui permet de récupérer les données tapées par l'utilisateur.

Le 2-way-binding


En Français, cela donne la "reliure bidirectionnelle". On comprendra que je préfère utiliser 2-way-binding :)

Arthur, l'apprenti développeurMoi aussi je préfère 2-way-binding. D'ailleurs je vais même dire 2WB pour que ce soit + court !
À ta guise, je ferai de même dans ce cas pour gagner du temps.

Arthur, l'apprenti développeurCependant... Je ne sais pas ce que c'est.
Le 2WB est une manière de lier la valeur tapée (input text, password, textarea...) ou choisie (select, input radio, checkbox...) à une donnée de notre composant (notre root component dans ce cours).
Pour cela, on va utiliser encore une fois quelque chose que tu connais bien... Une idée ?

Arthur, l'apprenti développeurUne directive ?
Exactement. Et j'ai le plaisir de te présenter v-model. Dans cette directive qui s'applique uniquement sur les champs de formulaire (input, select, textarea...), on renseigne simplement le nom de la donnée liée.
Voici un exemple qui utilise différents types de champs :

{"language":"text/html","content":"<body>\n\t<main id=\"app\">\n\t\t<span v-if=\"name\">Votre nom : {{ name }}</span>\n\t\t<div>\n\t\t\t<label for=\"name\">Votre nom</label>\n\t\t\t<input type=\"text\" v-model=\"name\" id=\"name\">\n\t\t</div>\n\t\t<div>\n\t\t\t<label for=\"age\">Votre âge</label>\n\t\t\t<select id=\"age\" v-model=\"age\">\n\t\t\t\t<option value=\"5\">moins de 5 ans</option>\n\t\t\t\t<option value=\"20\">moins de 20 ans</option>\n\t\t\t\t<option value=\"50\">moins de 50 ans</option>\n\t\t\t\t<option value=\"105\">moins de 105 ans</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<span v-if=\"age\">Vous avez moins de {{ age }} ans</span>\n\t\t<div>\n\t\t\tVous aimez les pommes ? <br />\n\t\t\t<div>\n\t\t\t\t<label for=\"yes\">oui</label>\n\t\t\t\t<input type=\"radio\" id=\"yes\" value=\"true\" v-model=\"apple\" />\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<label for=\"no\">non</label>\n\t\t\t\t<input type=\"radio\" id=\"no\" value=\"false\" v-model=\"apple\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<span v-if=\"apple\">Vous aimez les pommes</span>\n\t\t<span v-else>Vous n'aimez pas les pommes</span>\n\t</main>\n\t<script type=\"module\">\n\t\timport { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n\t\t\tdata() {\n\t\t\t return {\n\t\t\t \tname:'',\n\t\t\t \tage:0,\n\t\t\t \tapple:false\n\t\t\t }\n\t\t\t},\n\t\t}).mount('#app')\n\t</script>\n</body>","filename":"index.html"}

Arthur, l'apprenti développeurÇa marche super bien c'est pratique !
Oui, mais as-tu tout bien testé ? Je te laisse essayer le code...

Arthur, l'apprenti développeurQuelque chose ne devrait pas aller ?
En effet. Teste bien.

Arthur, l'apprenti développeurOh ! Quand on recoche "non" à la question des pommes, le texte ne se modifie pas en conséquence... Pourtant, le value de l'input est bien à false... Je ne comprends pas.
Pour te faire comprendre, voici un exemple. Avec celui-ci, il n'y a plus de problème :

{"language":"text/html","content":"<span v-if=\"apple == 'true'\">Vous aimez les pommes</span>\n<span v-else>Vous n'aimez pas les pommes</span>","filename":""}

Arthur, l'apprenti développeurEn effet ! Pourquoi ?
Parce qu'ici, tu remarqueras qu'on vérifie si apple est égal à la chaîne caractère "true", pas au booléen "true"... Tout le problème est là ! Pour palier à ce problème, nous devons utiliser des modificateurs.

Les modificateurs avec v-model


number


Ce modificateur permet de directement passer la valeur d'un input en nombre.
{"language":"text/html","content":"<input type=\"text\" v-on:change=\"log\" v-model.number=\"unNombre\">","filename":""}

Par défaut, si l'input est de type number, alors la donnée est automatiquement considérée comme number. Tu peux vérifier avec ce code :

{"language":"text/html","content":"<body>\n <main id=\"app\" class=\"px-4 pt-8 w-full flex flex-wrap\">\n <input type=\"number\" v-on:change=\"log\" v-model=\"unNombre\">\n </main>\n <script type=\"module\">\n import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n data() {\n return {\n unNombre: null\n }\n },\n methods:\n {\n log() {\n console.log(typeof this.unNombre)\n }\n }\n }).mount('#app')\n </script>\n</body>","filename":"index.html"}

trim


Ce modificateur permet d'enlever les espaces du début et de la fin qu'un utilisateur pourrait mettre dans un champ
{"language":"text/html","content":"<input type=\"text\" v-model.trim=\"textSansAucunEspaceAuDebutNiALaFin\">","filename":""}


lazy


Ce modificateur est plus particulier. Il faut savoir que le 2WB s'effectue à l'écoute de l'évènement input de JS. C'est à dire qu'à chaque fois que l'évènement "input" est lancé, la donnée spécifiée dans v-model est mise à jour. Si on préfère que le 2WB s'effectue lors de l'évènement "change" plutôt (qui est lancé moins souvent que "input"), il nous suffit d'utiliser lazy.
{"language":"text/html","content":"<input type=\"text\" v-model.lazy=\"...\">","filename":""}

Arthur, l'apprenti développeurSuper ! Maintenant on peut résoudre le problème du code avec l'input number. Mais comment fait-on pour notre histoire de pomme ? On n'a pas de modificateur .boolean ?
Non on en a pas... Mais en réalité, il y a une autre façon de caster. Je t'avais dit que v-bind servait à lier une donnée et un attribut HTML existant. Mais aussi que v-bind servait à d'autres choses... Hé bien, il sert aussi à caster. En effet, si on utilise v-bind sur un attribut, c'est qu'on attend une donnée en JS. Cela peut être une donnée de composant renseignée dans data et aussi n'importe quelle objet de JS natif, comme un nombre ou... un booléen :). Et donc, le type sera préservé.
Ce qu'il faut retenir, c'est que le type de la valeur de l'attribut qu'on a "v-bindé" gardera son type une fois traité par Vue.JS.

Ainsi, le problème venait du fait que la valeur de l'attribut value="true" était interprété comme une chaine de caractère classique. Si on utilise v-bind, ce sera interprété comme le booléen true en JS, donc on sera sauvé.
Résultat du code, qui fonctionne :

{"language":"text/html","content":"<body>\n\t<main id=\"app\">\n\t\t<span v-if=\"name\">Votre nom : {{ name }}</span>\n\t\t<div>\n\t\t\t<label for=\"name\">Votre nom</label>\n\t\t\t<input type=\"text\" v-model=\"name\" id=\"name\">\n\t\t</div>\n\t\t<div>\n\t\t\t<label for=\"age\">Votre âge</label>\n\t\t\t<select id=\"age\" v-model=\"age\">\n\t\t\t\t<option value=\"5\">moins de 5 ans</option>\n\t\t\t\t<option value=\"20\">moins de 20 ans</option>\n\t\t\t\t<option value=\"50\">moins de 50 ans</option>\n\t\t\t\t<option value=\"105\">moins de 105 ans</option>\n\t\t\t</select>\n\t\t</div>\n\t\t<span v-if=\"age\">Vous avez moins de {{ age }} ans</span>\n\t\t<div>\n\t\t\tVous aimez les pommes ? <br />\n\t\t\t<div>\n\t\t\t\t<label for=\"yes\">oui</label>\n\t\t\t\t<input type=\"radio\" id=\"yes\" :value=\"true\" v-model=\"apple\" />\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<label for=\"no\">non</label>\n\t\t\t\t<input type=\"radio\" id=\"no\" :value=\"false\" v-model=\"apple\" />\n\t\t\t</div>\n\t\t</div>\n\t\t<span v-if=\"apple\">Vous aimez les pommes</span>\n\t\t<span v-else>Vous n'aimez pas les pommes</span>\n\t</main>\n\t<script type=\"module\">\n\t\timport { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n\t\t\tdata() {\n\t\t\t return {\n\t\t\t \tname:'',\n\t\t\t \tage:0,\n\t\t\t \tapple:false\n\t\t\t }\n\t\t\t},\n\t\t}).mount('#app')\n\t</script>\n</body>","filename":"index.html"}

Compris ?

Arthur, l'apprenti développeurC'est compris, merci. Du coup on peut attaquer la suite de notre projet !
Tout à fait ! On va imaginer un formulaire simple, avec titre et contenu. Voici ce que ça donnerait :

{"language":"text/html","content":"<!DOCTYPE html>\n<html lang=\"fr\">\n\n<head>\n <meta charset=\"utf-8\">\n <title>Mes premiers pas avec Vue 3</title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <style>\n [v-cloak] {\n display: none;\n }\n\n .fade-enter-active,\n .fade-leave-active {\n transition: all 0.5s ease;\n }\n\n .fade-enter-from,\n .fade-leave-to {\n opacity: 0;\n }\n </style>\n</head>\n\n<body>\n <main id=\"app\" v-cloak>\n <h1 class=\"text-2xl py-4\">Les articles</h1>\n <Transition name=\"fade\">\n <div v-show=\"modal == 'open'\"\n class=\"w-1/2 h-1/2 bg-white px-2 py-2 z-10 overflow-y-auto shadow-md rounded fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\">\n <h2 class=\"text-xl py-4\">Mes articles en attente</h2>\n <a :href=\"`http://monsite.com/${post.id}`\" v-for=\"post in selection\" :key=\"post.id\" class=\"block\">{{\n post.title\n }}</a>\n </div>\n </Transition>\n <div class=\"w-1/2 mx-auto my-4\">\n <h2 class=\"text-xl py-4\">Ajouter un article</h2>\n <label for=\"title\" class=\"block text-sm font-medium text-gray-700\">Titre</label>\n <div class=\"mt-1\">\n <input type=\"text\" name=\"title\" id=\"title\" v-model=\"title\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\">\n </div>\n <label for=\"content\" class=\"block text-sm font-medium text-gray-700\">Contenu</label>\n <div class=\"mt-1\">\n <textarea rows=\"4\" v-model=\"content\" name=\"content\" id=\"content\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\"></textarea>\n </div>\n <div class=\"w-full text-right my-4\">\n <button type=\"button\" class=\"inline-flex items-center rounded border border-transparent bg-blue-100 px-2.5 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-200\">Ajouter</button>\n </div>\n \n </div>\n <div v-if=\"loading\">Chargement en cours...</div>\n\n <p v-if=\"posts.length < 1\"\n class=\"max-w-7xl mx-auto border-l-4 border-yellow-400 bg-yellow-50 p-4 text-sm text-yellow-700\">\n Aucun article à afficher.\n </p>\n <div v-else class=\"max-w-7xl mx-auto grid gap-4 grid-cols-3\">\n <article v-for=\"post in posts\" class=\"shadow px-4 pb-8 pt-2 rounded relative\" :key=\"post.id\">\n <a :href=\"`http://monsite.com/${post.id}`\" class=\"mt-4 block\">\n <h2 class=\"text-xl font-semibold text-gray-900\">{{ post.title }}</h2>\n <p class=\"mt-3 text-base text-gray-500\">{{ post.body }}</p>\n </a>\n <button @click=\"toggleSelection(post, $event.target)\" class=\"text-sm absolute bottom-2 px-2 rounded\"\n :class=\"[selection.includes(post) ? css.ButtonRemove : css.ButtonAdd]\">\n Ajouter à ma liste\n </button>\n </article>\n </div>\n <footer v-if=\"selection.length > 0\" class=\"fixed bottom-0 right-2 px-2 py-4 rounded bg-gray-300\">\n <button @click=\"modal='open'\">Voir {{ selection.length > 1 ? 'les' : '' }} {{ selection.length }}\n article{{selection.length > 1 ? 's' : '' }} à lire plus tard</button>\n </footer>\n </main>\n\n <script type=\"module\">\n import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n data() {\n return {\n loading: true,\n posts: [],\n selection: [],\n css: {\n ButtonAdd: 'text-green-700 bg-green-200 hover:bg-green-300',\n ButtonRemove: ['text-yellow-700 bg-yellow-100 hover:bg-yellow-200'],\n },\n modal: 'close',\n title: '',\n content: '',\n }\n },\n methods: {\n toggleSelection(post, button) {\n if (this.selection.includes(post)) {\n this.selection.splice(this.selection.indexOf(post), 1)\n button.textContent = 'Ajouter à ma liste'\n } else {\n this.selection.push(post)\n button.textContent = 'Enlever de ma liste'\n }\n }\n },\n beforeCreate() {\n this.loading = true;\n },\n created() {\n fetch('https://jsonplaceholder.typicode.com/posts')\n .then((response) => response.json())\n .then((json) => { this.posts = json });\n },\n mounted() {\n this.loading = false\n }\n }).mount('#app')\n\n </script>\n</body>\n\n</html>","filename":"index.html"}

Là notre formulaire est prêt. Il ne nous reste plus qu'à ajouter l'article au clic sur le bouton. On va utiliser une méthode pour ça :

{"language":"text/html","content":"<!DOCTYPE html>\n<html lang=\"fr\">\n\n<head>\n <meta charset=\"utf-8\">\n <title>Mes premiers pas avec Vue 3</title>\n <script src=\"https://cdn.tailwindcss.com\"></script>\n <style>\n [v-cloak] {\n display: none;\n }\n\n .fade-enter-active,\n .fade-leave-active {\n transition: all 0.5s ease;\n }\n\n .fade-enter-from,\n .fade-leave-to {\n opacity: 0;\n }\n </style>\n</head>\n\n<body>\n <main id=\"app\" v-cloak>\n <h1 class=\"text-2xl py-4\">Les articles</h1>\n <Transition name=\"fade\">\n <div v-show=\"modal == 'open'\"\n class=\"w-1/2 h-1/2 bg-white px-2 py-2 z-10 overflow-y-auto shadow-md rounded fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2\">\n <h2 class=\"text-xl py-4\">Mes articles en attente</h2>\n <a :href=\"`http://monsite.com/${post.id}`\" v-for=\"post in selection\" :key=\"post.id\" class=\"block\">{{\n post.title\n }}</a>\n </div>\n </Transition>\n <div class=\"w-1/2 mx-auto my-4\">\n <h2 class=\"text-xl py-4\">Ajouter un article</h2>\n <label for=\"title\" class=\"block text-sm font-medium text-gray-700\">Titre</label>\n <div class=\"mt-1\">\n <input type=\"text\" name=\"title\" id=\"title\" v-model=\"title\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\">\n </div>\n <label for=\"content\" class=\"block text-sm font-medium text-gray-700\">Contenu</label>\n <div class=\"mt-1\">\n <textarea rows=\"4\" v-model=\"content\" name=\"content\" id=\"content\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\"></textarea>\n </div>\n <div class=\"w-full text-right my-4\">\n <button type=\"button\" @click=\"addPost\"\n class=\"inline-flex items-center rounded border border-transparent bg-blue-100 px-2.5 py-1.5 text-xs font-medium text-blue-700 hover:bg-blue-200\">Ajouter</button>\n </div>\n\n </div>\n <div v-if=\"loading\">Chargement en cours...</div>\n\n <p v-if=\"posts.length < 1\"\n class=\"max-w-7xl mx-auto border-l-4 border-yellow-400 bg-yellow-50 p-4 text-sm text-yellow-700\">\n Aucun article à afficher.\n </p>\n <div v-else class=\"max-w-7xl mx-auto grid gap-4 grid-cols-3\">\n <article v-for=\"post in posts\" class=\"shadow px-4 pb-8 pt-2 rounded relative\" :key=\"post.id\">\n <a :href=\"`http://monsite.com/${post.id}`\" class=\"mt-4 block\">\n <h2 class=\"text-xl font-semibold text-gray-900\">{{ post.title }}</h2>\n <p class=\"mt-3 text-base text-gray-500\">{{ post.body }}</p>\n </a>\n <button @click=\"toggleSelection(post, $event.target)\" class=\"text-sm absolute bottom-2 px-2 rounded\"\n :class=\"[selection.includes(post) ? css.ButtonRemove : css.ButtonAdd]\">\n Ajouter à ma liste\n </button>\n </article>\n </div>\n <footer v-if=\"selection.length > 0\" class=\"fixed bottom-0 right-2 px-2 py-4 rounded bg-gray-300\">\n <button @click=\"modal='open'\">Voir {{ selection.length > 1 ? 'les' : '' }} {{ selection.length }}\n article{{selection.length > 1 ? 's' : '' }} à lire plus tard</button>\n </footer>\n </main>\n\n <script type=\"module\">\n import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n data() {\n return {\n loading: true,\n posts: [],\n selection: [],\n css: {\n ButtonAdd: 'text-green-700 bg-green-200 hover:bg-green-300',\n ButtonRemove: ['text-yellow-700 bg-yellow-100 hover:bg-yellow-200'],\n },\n modal: 'close',\n title: '',\n content: '',\n }\n },\n methods: {\n addPost() {\n this.posts.push({id: this.posts.length+1, title:this.title, body:this.content})\n },\n toggleSelection(post, button) {\n if (this.selection.includes(post)) {\n this.selection.splice(this.selection.indexOf(post), 1)\n button.textContent = 'Ajouter à ma liste'\n } else {\n this.selection.push(post)\n button.textContent = 'Enlever de ma liste'\n }\n }\n },\n beforeCreate() {\n this.loading = true;\n },\n created() {\n fetch('https://jsonplaceholder.typicode.com/posts')\n .then((response) => response.json())\n .then((json) => { this.posts = json });\n },\n mounted() {\n this.loading = false\n }\n }).mount('#app')\n\n </script>\n</body>\n\n</html>","filename":"index.html"}

Et voilà ! Que dirais-tu de vérifier les entrées des utilisateurs et d'afficher des messages d'erreur s'il y a lieu ? C'est le but du prochain chapitre : utiliser les propriétés calculées et les observateurs ;) J'ai terminé cette partie
Demander de l'assistance