Commentaires
Ces 2 notions sont souvent abordées ensemble, c'est pour cela que je vais te les présenter ensemble.

Arthur, l'apprenti développeurD'ailleurs, elles sont aussi souvent proposées bien plus tôt dans les cours sur Vue que j'ai pu trouver autre part sur le web.
Tu as tout à fait raison. Mais ce cours, contrairement à beaucoup d'autres, est orienté sur la pratique ! Du coup, on voit dans l'ordre qui nous arrange :D

Arthur, l'apprenti développeurEt ça me va très bien comme ça !
Je suis heureux de l'entendre.

Les propriétés calculées (computed properties)


Les propriétés calculées sont des fonctions qui retournent des valeurs, tout simplement. Comme beaucoup de fonctions me diras-tu. La différence réside dans le fait que ces fonctions retournent des valeurs basées sur des données renseignées dans data(). En général, on utilise les computed properties (CP) pour simplifier l'écriture dans le HTML tout simplement. Prenons un exemple tout simple.

{"language":"text/html","content":"<main id=\"app\" v-cloak>\n <span>{{ name.length > 0 ? \"Votre nom est : \" + name : \"Vous n'avez pas encore renseigné votre nom \"}}</span>\n <div>\n <label for=\"name\">Votre nom</label>\n <input type=\"text\" v-model=\"name\" id=\"name\">\n </div>\n\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 name: '',\n }\n }\n })\n</script>","filename":"index.html"}

Dans ce code, on cherche à afficher "Votre nom est..." si la personne à renseigner des lettres, sinon on cherche à afficher "Vous n'avez pas encore renseigné de nom". Bon, on remarque vite que dans nos accolades {{ }}, ça fait long. Du coup, pour raccourcir tout ça, on va utiliser une CP !

Les CP se mettent dans un objet comme les méthodes et fonctionnent comme les méthodes. La différence réside dans le fait que l'objet se nomme "computed" et non pas "methods". Une fois qu'on a écrit notre CP, on peut utiliser son nom comme une donnée de data dans nos moustaches {{ }}.

{"language":"text/html","content":"<main id=\"app\" v-cloak>\n <span>{{ message }}</span>\n <div>\n <label for=\"name\">Votre nom</label>\n <input type=\"text\" v-model=\"name\" id=\"name\">\n </div>\n\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 name: '',\n }\n },\n computed: {\n message() {\n return this.name.length > 0 ? \"Votre nom est : \" + this.name : \"Vous n'avez pas encore renseigné votre nom \"\n }\n }\n }).mount('#app')\n</script>","filename":"index.html"}

Pratique non ?

Arthur, l'apprenti développeurPratique ! Mais on ne pouvait pas utiliser une méthode à la place et faire {{ maMethode() }} ?
On aurait pu. Mais la valeur d'une CP est mise à jour uniquement lorsqu'une des données qu'elle traite est mise à jour. Alors qu'une méthode s'exécutera bien plus souvent. C'est ce qu'on appelle le computed caching.
Cela signifie par contre que les CP ne sont réactives qu'aux données du data, pas à autre chose. Ainsi, rien ne sert d'utiliser des fonctions comme Date.now() etc car elles ne sont pas des données de Vue, donc les CP ne se mettront jamais à jour !

Arthur, l'apprenti développeurC'est noté !
Avec les CP, on va pouvoir améliorer notre projet. Tu sais, on a un bouton avec pas mal de variables :

{"language":"text/html","content":"<button @click=\"modal='open'\">\n Voir {{ selection.length > 1 ? 'les' : '' }} {{ selection.length }} article{{selection.length > 1 ? 's' : '' }} à lire plus tard\n</button>","filename":"index.html"}

On peut l'améliorer comme suit :

{"language":"text/html","content":"<button @click=\"modal='open'\">Voir {{ nbPosts }} à lire plus tard</button>\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 computed: {\n nbPosts() {\n return `${this.selection.length > 1 ? 'les' : ''} ${this.selection.length} article ${this.selection.length > 1 ? 's' : ''}`\n }\n },\n //...\n \n }).mount('#app')\n\n</script>","filename":"index.html"}

Arthur, l'apprenti développeurC'est vrai que ça parait plus propre !

Les observateurs (watchers)


Les observateurs sont présentés conjointement avec CP dans la doc et en général. Cependant, ils n'ont pas le même rôle. Les Watchers servent à réagir en cas de changement de données. Pratique avec le 2-way-binding car on peut donc réagir aux changements sur les inputs. Et donc... Vérifier leur valeur et afficher des messages d'erreur en temps réel.
Les observateurs se mettent dans un objet watch. Ensuite, on doit écrire des méthodes qui portent le même nom que la donnée qu'on observe. Ces méthodes prennent deux paramètres dont un facultatif :

  • La nouvelle valeur de la donnée modifiée

  • L'ancienne valeur de la donnée modifiée (facultatif)


Dans notre projet, on veut afficher des messages d'erreur sous les input s'il y a des problèmes. On va donc déjà créer un objet qui contiendra les messages d'erreur. Je l'appelle "errors".

{"language":"text/html","content":"<script type=\"module\">\n import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'\n createApp({\n data() {\n return {\n // Mes autres data...\n errors: {},\n }\n }\n })\n</script>","filename":"index.html"}

Ensuite, en dessous de chaque input je mets des span qui affichent les messages d'erreur.

{"language":"text/html","content":"<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 <span v-if=\"errors.title\" class=\"text-sm text-red-400\">{{ errors.title }} </span>\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 <span v-if=\"errors.content\" class=\"text-sm text-red-400\">{{ errors.content }} </span>\n</div>","filename":"index.html"}


Et maintenant les watchers. Qu'aimerais-tu ajouter comme règles sur nos champs de formulaire ?

Arthur, l'apprenti développeurAlors :
  • Le titre doit avoir entre 3 et 20 caractères
  • Le contenu doit avoir minimum 5 caractères


Très bien, faisons ça. Je te montre le watcher pour le titre et tu pourras faire celui du contenu.

{"language":"text/javascript","content":"watch: {\n title(newValue) {\n this.errors.title = ''\n if(newValue.length < 3 || newValue.length > 20) {\n this.errors.title = 'Le titre doit avoir entre 3 et 20 caractères'\n }\n },\n}","filename":"vue.js"}

Arthur, l'apprenti développeurTon exemple m'aide bien. Je viens de penser à quelque chose : je vais ajouter le modificateur trim sur les v-model pour ne pas compter des éventuels espaces en début ou fin de mot.
Très bonne idée. Voici donc le code final que je te propose. J'ai ajouté une partie pour disable le bouton dans le cas où l'un ou l'autre des champs du formulaire n'aurait pas une valeur correcte.

{"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.trim=\"title\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\">\n <span v-if=\"errors.title\" class=\"text-sm text-red-400\">{{ errors.title }} </span>\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.trim=\"content\" name=\"content\" id=\"content\"\n class=\"block w-full rounded-md border-gray-300 shadow-sm\"></textarea>\n <span v-if=\"errors.content\" class=\"text-sm text-red-400\">{{ errors.content }} </span>\n </div>\n <div class=\"w-full text-right my-4\">\n <button type=\"button\" @click=\"addPost\" :disabled=\"formHasErrors\"\n class=\"disabled:bg-gray-200 disabled:opacity-75 disabled:text-white 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 {{ nbPosts }} à 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 errors: {\n title:null,\n content:null\n },\n }\n },\n computed: {\n nbPosts() {\n return `${this.selection.length > 1 ? 'les' : ''} ${this.selection.length} article${this.selection.length > 1 ? 's' : ''}`\n },\n formHasErrors() {\n return (this.errors.title != '' || this.errors.content != '')\n }\n },\n watch: {\n title(newValue) {\n this.errors.title = ''\n if(newValue.length < 3 || newValue.length > 20) {\n this.errors.title = 'Le titre doit avoir entre 3 et 20 caractères'\n }\n },\n content(newValue) {\n // A toi de jouer\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"}

Arthur, l'apprenti développeurC'est super, ça marche bien !
En effet. Une dernière chose avant de terminer cette partie : je t'avais dit qu'il y avait encore une autre façon de gérer les styles. On peut en effet utiliser une CP !

{"language":"text/html","content":"<div :class=\"classObject\"></div>\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 valid: false,\n }\n },\n computed: {\n classObject() {\n return {\n 'bg-red-200 text-red-400':!valid,\n }\n }\n }\n }).mount('#app')\n</script>","filename":"index.html"}

Tu es arrivé à la fin des compétences techniques à acquérir durant ce cours. Voyons maintenant vers quoi t'orienter pour continuer ton apprentissage car il est loin d'être terminé !

Arthur, l'apprenti développeurDis-moi tout ! J'ai terminé cette partie
Demander de l'assistance