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 :)

À ta guise, je ferai de même dans ce cas pour gagner du temps.

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 ?

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"}

Oui, mais as-tu tout bien testé ? Je te laisse essayer le code...

En effet. Teste bien.

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":""}

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":""}

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 ?

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