Commentaires
On va maintenant voir les composants dynamiques.

Arthur, l'apprenti développeurLes composants qu'on a déjà faits ne sont pas dynamiques ?
Non. Enfin si. Mais pas dans ce sens ! Le plus simple pour te faire comprendre ce qu'ils sont c'est cet exemple :

{"language":"text/html","content":"<component :is=\"photo\"></component> <!-- Semblable à <photo></photo> -->","filename":""}

Arthur, l'apprenti développeurMais quel est l'intérêt d'utiliser ce nouvel attribut v-bindé "is" et cette balise component plutôt que d'utiliser directement <photo> ?
En fait c'est très pratique quand on veut passer d'un composant à un autre, dans le cas par exemple d'une fiche à onglets (en Anglais "tabs"). C'est d'ailleurs le cas d'utilisation le plus commun pour présenter les composants dynamiques.
C'est parfait car cela colle très bien à notre projet. Nous allons faire deux onglets. Un onglet qui contiendra un formulaire pour ajouter son avis sur la photo. Un onglet qui contiendra les avis déjà rédigés.

Nos onglets composants


Nos onglets seront au nombre de 2 comme dit précédemment.
Je rappelle que les onglets sont obligatoirement des composants.
Le premier que je te propose est tout simple, c'est celui qui permettra d'afficher les avis.

{"language":"text/html","content":"<template>\n\t<ul>\n\t\t<li v-for=\"view in views\" :key=\"view\">\n\t\t\t<h3>{{ view.author }}</h3>\n\t\t\t<p>{{ view.content }}</p>\n\t\t</li>\n\t</ul>\n</template>\n\n<script>\n\texport default{\n\t\tprops: {\n\t\t\tviews: {\n\t\t\t\ttype: Array,\n\t\t\t\trequired:true\n\t\t\t}\n\t\t}\n\t}\n</script>","filename":"Views.vue"}

Le second est le formulaire d'ajout d'avis :

{"language":"text/html","content":"<template>\n\t<h3 id=\"title\">Ajouter un avis</h3>\n\t<div class=\"row_form\">\n\t\t<label for=\"author\">Votre nom : </label>\n\t\t<input id=\"author\"/>\n\t</div>\n\t<div class=\"row_form\">\n\t\t<label for=\"content\">Votre avis :</label>\n\t\t<textarea id=\"content\"></textarea>\n\t</div>\n\t<div class=\"row_form\">\n\t\t<button class=\"button centered\" type=\"submit\">Soumettre l'avis</button>\n\t</div>\n\n</template>\n\n<style scoped>\n\t#title{\n\t\ttext-align:center;\n\t}\n\t.row_form{\n\t\tdisplay:flex;\n\t\tflex-wrap:wrap;\n\t\tjustify-content: center;\n\t\tflex-direction: column;\n\t\twidth:80%;\n\t\tmargin:1em auto;\n\t}\n</style>","filename":"ViewForm.vue"}

Arthur, l'apprenti développeurTu n'as pas ajouté de v-model ?
Pas encore en effet. Je les ajoute de suite ! N'oublie pas cependant qu'il va falloir passer ces données au parent (ici Photo) pour les ajouter en tant qu'avis !

{"language":"text/html","content":"<template>\n\t<h3 id=\"title\">Ajouter un avis</h3>\n\t<div class=\"row_form\">\n\t\t<label for=\"author\">Votre nom : </label>\n\t\t<input id=\"author\" v-model=\"author\"/>\n\t</div>\n\t<div class=\"row_form\">\n\t\t<label for=\"content\">Votre avis :</label>\n\t\t<textarea id=\"content\" v-model=\"content\"></textarea>\n\t</div>\n\t<div class=\"row_form\">\n\t\t<button class=\"button centered\" type=\"submit\" @click=\"submit\">Soumettre l'avis</button>\n\t</div>\n\n</template>\n\n<script>\nexport default {\n\temits:['addView'],\n\tdata() {\n\t\treturn {\n\t\t\tcontent:'',\n\t\t\tauthor:''\n\t\t}\n\t}, \n\tmethods: {\n\t\tsubmit() {\n\t\t\tthis.$emit('addView', this.author, this.content)\n\t\t\tthis.author = ''\n\t\t\tthis.content = ''\n\t\t}\n\t}\n}\n</script>\n<style scoped>\n\t#title{\n\t\ttext-align:center;\n\t}\n\t.row_form{\n\t\tdisplay:flex;\n\t\tflex-wrap:wrap;\n\t\tjustify-content: center;\n\t\tflex-direction: column;\n\t\twidth:80%;\n\t\tmargin:1em auto;\n\t}\n</style>","filename":"ViewForm.vue"}


Ensuite, il faut modifier notre composant Photo pour ajouter ces onglets. Le modifier également pour intégrer une nouvelle data qui sera le tableau d'avis, et rajouter la méthode qui se déclenche lorsque le "emit" de ViewForm.vue est envoyé !


{"language":"text/html","content":"<template>\n <figure class=\"photo\">\n <img :src=\"url\" :alt=\"title + 'prise par' +copyright\" />\n <figcaption>\n <h2>{{ title }} prise le {{ formattedDate }} par {{ copyright }}</h2>\n <p>{{ explanation }}</p>\n </figcaption>\n </figure>\n <component :is=\"???\" @add-view=\"storeView\"></component>\n</template>\n\n<script>\nimport Views from './Views.vue'\nimport ViewForm from './ViewForm.vue'\n\nexport default {\n components:{\n Views, ViewForm\n },\n props:{\n url: {\n type:String, \n required:true,\n default:'http://uneimagepardefaut.png'\n },\n title: {\n type:String,\n required:true,\n },\n date: {\n type:String,\n required:true,\n default:Date.now()\n },\n explanation: {\n type:String,\n required:true,\n },\n copyright: {\n type:String,\n required:false,\n default:'un inconnu'\n }\n },\n data() {\n return {\n views: [ {\n author:'Antoine',\n content:'Super photo'\n }\n ]\n }\n },\n computed: {\n formattedDate() {\n let date = new Date(this.date)\n let day = Number(date.getDate()) >= 10 ? date.getDate() : '0'+date.getDate()\n return `${day}/${date.getMonth()}/${date.getFullYear()}`\n },\n }, \n methods:{\n storeView(author, content) {\n this.views.push({author:author, content:content})\n }\n }\n}\n</script>\n\n<style scoped>\n .photo img{\n max-height:300px;\n }\n\n .photo{\n text-align:center;\n }\n</style>","filename":"Photo.vue"}


Comme tu le vois, il faut maintenant qu'on fasse la logique pour changer de composant et donc trouver ce qu'on met à la place de nos "???" dans :is="". C'est parti !

Passer d'un onglet à un autre


Pour ça il suffit de définir une data qui contient le nom de l'onglet (donc le nom du composant) courant et de créer des boutons qui modifient cette data.

{"language":"text/html","content":"<template>\n <figure class=\"photo\">\n <img :src=\"url\" :alt=\"title + 'prise par' +copyright\" />\n <figcaption>\n <h2>{{ title }} prise le {{ formattedDate }} par {{ copyright }}</h2>\n <p>{{ explanation }}</p>\n </figcaption>\n </figure>\n <button @click=\"currentTab = 'Views'\">Avis</button>\n <button @click=\"currentTab = 'ViewForm'\">Rédiger un avis</button>\n <component :is=\"currentTab\" @add-view=\"storeView\"></component>\n</template>\n\n<script>\nimport Views from './Views.vue'\nimport ViewForm from './ViewForm.vue'\n\nexport default {\n components:{\n Views, ViewForm\n },\n props:{\n url: {\n type:String, \n required:true,\n default:'http://uneimagepardefaut.png'\n },\n title: {\n type:String,\n required:true,\n },\n date: {\n type:String,\n required:true,\n default:Date.now()\n },\n explanation: {\n type:String,\n required:true,\n },\n copyright: {\n type:String,\n required:false,\n default:'un inconnu'\n }\n },\n data() {\n return {\n\t\t\tcurrentTab:'Views',\n views: [ {\n author:'Antoine',\n content:'Super photo'\n }\n ]\n }\n },\n computed: {\n formattedDate() {\n let date = new Date(this.date)\n let day = Number(date.getDate()) >= 10 ? date.getDate() : '0'+date.getDate()\n return `${day}/${date.getMonth()}/${date.getFullYear()}`\n },\n }, \n methods:{\n storeView(author, content) {\n this.views.push({author:author, content:content})\n }\n }\n}\n</script>\n\n<style scoped>\n .photo img{\n max-height:300px;\n }\n\n .photo{\n text-align:center;\n }\n</style>","filename":"Photo.vue"}

Arthur, l'apprenti développeurJe comprends l'idée ! Mais du coup j'ai une erreur avec ce code car on ne passe pas la props "views" à notre composant dynamique... D'ailleurs comment fait-on ?
Excellente remarque. En effet, nos deux composants sont maintenant bien dynamiques, mais comment faire pour leur passer des props ? Notamment, dans le cas du composant "Views", comment faire pour lui passer les avis de la photo ?
En général pour passer des props à des composants dynamiques on va utiliser notre directive préférée, v-bind, couplée à une computed property comme suit :

{"language":"text/html","content":"<template>\n <!-- ... -->\n <component :is=\"currentTab\" v-bind=\"dynamicProps\" @add-view=\"storeView\"></component>\n</template>\n\n<script>\nimport Views from './Views.vue'\nimport ViewForm from './ViewForm.vue'\n\nexport default {\n components:{\n Views, ViewForm\n },\n props:{\n // ...\n },\n data() {\n return {\n currentTab:'Views',\n views: [ {\n author:'Antoine',\n content:'Super photo'\n }\n ]\n }\n },\n computed: {\n formattedDate() {\n let date = new Date(this.date)\n let day = Number(date.getDate()) >= 10 ? date.getDate() : '0'+date.getDate()\n return `${day}/${date.getMonth()}/${date.getFullYear()}`\n },\n dynamicProps() {\n if(this.currentTab == 'Views') {\n return { views:this.views }\n }\n return null\n }\n }, \n methods:{\n storeView(author, content) {\n this.views.push({author:author, content:content})\n }\n }\n}\n</script>\n\n<style scoped>\n /** ... **/\n</style>","filename":"Photo.vue"}

Arthur, l'apprenti développeurAh oui on utilise v-bind avec la syntaxe objet comme vu quand on a passé les props au composant "Photo" !
C'est tout à fait ça. Ça nous permet de passer des props différentes selon le composant qui sera rendu par <component> !
Je te laisse essayer tout ça. Ça fonctionne bien ?

Arthur, l'apprenti développeurOui ! On a bien nos deux onglets qui fonctionnent ;)
Exact, mais on a un warning dans la console :
"[Vue warn]: Extraneous non-props attributes (hdurl, media_type, service_version) were passed to component but could not be automatically inherited because component renders fragment or text root nodes."
C'est parce qu'on utilise v-bind="photo" donc que certains éléments sont passés sous forme de non-prop attributes et que nous avons plusieurs enfants de premier niveau dans le template de notre composant Photo.vue.

Arthur, l'apprenti développeurEt alors ?
Rappelle-toi dans le chapitre sur les "props" ce qu'il advenait des non-props attributes : ils étaient ajoutés sur la balise <figure>. Sais-tu pourquoi ? Hé bien parce que c'était la balise qui englobait le contenu de notre composant. Mais maintenant que nous avons plusieurs enfants de premier niveau dans notre composant Photo, Vue ne sait pas où mettre ces non-props attributes !
On peut lui indiquer grâce à $attrs.

Arthur, l'apprenti développeurAh oui tu m'avais dit que ça nous serait utile !
Il nous suffit de mettre sur la balise sur laquelle on souhaite qu'il y ait les non-props attributes grâce à v-bind (vraiment géniale cette directive...). Je veux ici les mettre sur la balise figure :

{"language":"text/html","content":"<template>\n <figure class=\"photo\" v-bind=\"$attrs\">\n <img :src=\"url\" :alt=\"title + 'prise par' +copyright\" />\n <figcaption>\n <h2>{{ title }} prise le {{ formattedDate }} par {{ copyright }}</h2>\n <p>{{ explanation }}</p>\n </figcaption>\n </figure>\n <button @click=\"currentTab = 'Views'\">Avis</button>\n <button @click=\"currentTab = 'ViewForm'\">Rédiger un avis</button>\n <component :is=\"currentTab\" v-bind=\"dynamicProps\" @add-view=\"storeView\"></component>\n</template>","filename":"Photo.vue"}

Et voilà, on n'a plus de warning et tout fonctionne !

As-tu des questions ?

Arthur, l'apprenti développeurOui, j'ai une question... pourquoi ? Pourquoi utiliser des composants dynamiques alors qu'on aurait très bien pu faire la même chose avec v-if et sans créer de composants ViewForm et Views ?
Ta question est tout à fait légitime ! En fait, tu as une partie de la réponse : parce qu'avec v-if on n'aurait pas pu utiliser de composants. On n'aurait pas pu déléguer les tâches du formulaire à ViewForm par exemple. Bon, si ça te convainc pas, voici une autre bonne raison d'utiliser les composants dynamiques : la "mise en cache".

Arthur, l'apprenti développeurLa mise en cache ?
Oui. Actuellement si tu commences à rédiger un avis mais que tu ne soumets pas le formulaire puis retourne sur l'onglet "avis" tout le contenu du formulaire a disparu. Si tu t'es senti envahi par une âme littéraire et que tu as produit 100 lignes d'avis, c'est un peu dommage de tout voir disparaitre...
Heureusement, on peut, grâce aux composants dynamiques, garder l'état des composants comme ils étaient juste avant de changer d'onglet. Autrement dit dans notre cas on peut garder le contenu du formulaire même si on change d'onglet ;). Tout ça en ajoutant une balise pour "garder en vie" les onglets. (Ceci ne serait pas possible avec des simples v-if...). Cette balise est <keep-alive>. On la met autour de la balise <component>.

Voici donc notre composant Photo tout beau tout fini (avec un peu de style en plus) :

{"language":"text/html","content":"<template>\n <figure class=\"photo\" v-bind=\"$attrs\">\n <img :src=\"url\" :alt=\"title + 'prise par' +copyright\" />\n <figcaption>\n <h2>{{ title }} prise le {{ formattedDate }} par {{ copyright }}</h2>\n <p>{{ explanation }}</p>\n </figcaption>\n </figure>\n <button @click=\"currentTab = 'Views'\" class=\"button tab\" :class=\"{underline:(currentTab == 'Views')}\">Avis</button>\n <button @click=\"currentTab = 'ViewForm'\" class=\"button tab\" :class=\"{underline:(currentTab == 'ViewForm')}\">Rédiger un avis</button>\n <keep-alive>\n <component :is=\"currentTab\" v-bind=\"dynamicProps\" @add-view=\"storeView\"></component>\n </keep-alive>\n</template>\n\n<script>\nimport Views from './Views.vue'\nimport ViewForm from './ViewForm.vue'\n\nexport default {\n components:{\n Views, ViewForm\n },\n props:{\n url: {\n type:String, \n required:true,\n default:'http://uneimagepardefaut.png'\n },\n title: {\n type:String,\n required:true,\n },\n date: {\n type:String,\n required:true,\n default:Date.now()\n },\n explanation: {\n type:String,\n required:true,\n },\n copyright: {\n type:String,\n required:false,\n default:'un inconnu'\n }\n },\n data() {\n return {\n currentTab:'Views',\n views: []\n }\n },\n computed: {\n formattedDate() {\n let date = new Date(this.date)\n let day = Number(date.getDate()) >= 10 ? date.getDate() : '0'+date.getDate()\n return `${day}/${date.getMonth()}/${date.getFullYear()}`\n },\n dynamicProps() {\n if(this.currentTab == 'Views') {\n return { views:this.views }\n }\n return null\n }\n }, \n methods:{\n storeView(author, content) {\n this.views.push({author:author, content:content})\n }\n }\n}\n</script>\n\n<style scoped>\n .underline{\n text-decoration: underline\n }\n .tab{\n display:inline-block;\n border-radius: 20px 20px 0 0;\n }\n\n .photo img{\n max-height:300px;\n }\n\n .photo{\n text-align:center;\n }\n</style>","filename":"Photo.vue"}


Arthur, l'apprenti développeurSuper ! Je comprends mieux l'intérêt des composants dynamiques. Et c'est super keep-alive ! Notre projet est donc terminé ?
Il le pourrait ! Mais non.
On va terminer ce projet en imaginant que nous laissons la possibilité d'éditer la description des photos. Évidemment, ce sera purement local et nous n'enverrons aucune modification vers l'API de la NASA ! C'est pour voir quelque chose de super important parfois oublié dans les cours en ligne : le problème de v-model sur des props.

Arthur, l'apprenti développeurOk je te suis ! J'ai terminé cette partie
Demander de l'assistance