1°/ Les composants
1.1°/ préambule
- A part les directives permettant de dynamiser facilement une page, la grande force de vuejs est la possibilité de créer des composants réutilisables, qui peuvent donc être inclus les uns dans les autres pour former des composants plus complexes.
- Structurellement, un composant ressemble fortement à ce que l'on écrit pour instancier un vue sauf que l'on définit plutôt une sorte de classe avec ses propres données, ses méthodes et du code HTML, appelé template. Ce template peut bien entendu contenir des directives vuejs et faire appel à d'autres composants.
- Le comportement et l'apparence d'un composant peut être paramétré grâce à ses propriétés d'entrée, appelées props.
- instancier un composant revient simplement à inclure dans du code HTML (soit celui de la page principale, soit celui du template d'un autre composant) une balise qui porte le nom du composant et à donner des valeurs à des attributs qui représentent les props.
- Grâce à ce système, on peut créer autant d'instances que l'on veut d'un composant, avec à chaque fois des valeurs de props différentes.
- On peut également récupérer des composants développés par d'autres personnes et les utiliser dans ses projets. Cela dit, la plupart de ces types de composants sont implémentés avec une structure spéciale qui est compatible avec les environnements de développement vuejs (par ex vue-cli) mais pas avec du code "from scratch" comme on le fait dans ce cours.
- Pour déclarer un composant, il existe deux principes qui en pratique reviennent au même.
- la déclaration "locale" consiste à définir dans un fichier js (celui de la vue ou un autre) un objet JSON représentant le composant puis de spécifier dans l'attribut components de la vue que l'on utilise cette variable comme un composant. Cela permet de lui donner un nom "local" donc propre à l'instance de vue. La démonstration est fait dans la section suivante.
- la déclaration "globale" consiste à ajouter à la classe Vue la définition du composant. Il n'y a donc pas besoin d'utiliser l'attribut components puisque le composant fait en gros déjà partie de toutes les instance de vue.
1.2°/ Déclaration locale
- Dans cet exemple, la déclaration du composant se fait dans le même fichier que la vue, mais cela peut être fait dans un autre fichier, inclus avant.
Démonstration :
- créer un fichier index-01.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var myTitle = { props: [ 'title' ], template: "<h1>{{title}}</h1>" }; var app = new Vue({ el: '#mydiv', data: { items }, components: { shopTitle : myTitle /* we can also change the declaration name . e.g. myTitle In this case, the code in exe-01.html should use my-title instead of shop-title */ } }) |
- créer un fichier model.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var itemCats = [ 'helmet', 'crown', 'armor', 'clothes', 'weapon', 'lighter', 'purse', 'potion', 'spell', 'food']; var items = [ {name:'helmet', cat:'helmet', price:200}, {name:'broigne', cat:'armor', price:200}, {name:'hauberk', cat:'armor', price:500}, {name:'dagger', cat:'weapon', price:100}, {name:'long sword', cat:'weapon', price:300}, {name:'torch', cat:'lighter', price:2}, {name:'protection potion', cat:'potion', price:100}, {name:'fireball', cat:'spell', price:1000}, {name:'invisibility', cat:'spell', price:1000}, {name:'apple', cat:'food', price:1}, {name:'beef', cat:'food', price:5}, ]; |
- créer un fichier exe-01.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="model.js"></script> </head> <body> <div id="mydiv"> <shop-title title="Shop proposes:"></shop-title> <ol> <li v-for="it in items">{{it.name}} : {{it.price}} gp</li> </ol> </div> <script src="index-01.js"></script> </body> </html> |
Remarques :
- la variable myTitle permet de déclarer le composant avec deux attributs : les props et le template.
- l'instance de vue utilise cette variable pour définir localement un composant nommé shopTitle. On pourrait très bien laisser le même nom, auquel cas il suffit de mettre myTitle (comme avec les variables de data qui observent une variable de même nom).
- Pour créer une instance de ce composant, on crée une balise <shop-item> dans le HTML.
- ATTENTION ! en HTML, il n'y a pas de distinctions majuscules/minuscules. C'est pourquoi vuejs crée des alias pour tous les noms de composants qui ne suivent pas la notation dite kebab-case. Par exemple, en notation dite camelCase (façon java), une variable nommée mySuperVar sera nommée en kebab-case my-super-var.
- Pour spécifier une valeur fixe à la props nommé title, il suffit de donner une valeur à l'attribut title.
1.3°/ Déclaration globale
- Dans cet exemple, la délcarartion du composant est faîte dans un fichier annexe, inclus au début de la page HTML.
- Le composant prend 2 props : le titre et les items de la liste à afficher.
Démonstration :
- créer un fichier component-02.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 |
Vue.component("myItemList", { props: [ 'title', 'itemList' ], template: ` <div>\ <h1>{{title}}</h1> \ <ol> \ <li v-for="it in itemList">{{it.name}} : {{it.price}} gp</li> \ </ol> \ </div> ` }); |
- créer un fichier index-02.js et copier/coller dedans :
1 2 3 4 5 6 |
var app = new Vue({ el: '#mydiv', data: { items } }) |
- créer un fichier exe-02.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="model.js"></script> <script src="components-02.js"></script> </head> <body> <div id="mydiv"> <my-item-list title="Shop proposes:" :item-list="items"></my-item-list> </div> <script src="index-02.js"></script> </body> </html> |
Remarques :
- ATTENTION ! le template doit contenir un unique élément racine. Cela implique que si le template est composé de plusieurs balises, il faut toutes les inclure dans une balise <div>.
- Pour rendre le code plus lisible, il est possible de spécifier le template sur plusieurs lignes. Cependant, les navigateurs ne sont pas tous compatibles avec cette structure. Pour ceux qui le sont, il faut mettre le code entre anti-quote et à la fin de chaque ligne, ajouter un backslash (optionnel dans certains navigateurs).
- Comme dans la démoinstration 1, une valeur fixe est affectée à la props title.
- En revanche, s'il faut fournir une valeur observée par la vue, il faut utiliser v-bind. En l'occurence, on veut que la props itemList soit affectée avec la valeur de la variable observée items, d'où le :item-list="items"
1.4°/ propriétés du composant
- Comme pour une vue, un composant peut avoir son propre ensemble de propriétés observée.
- La seule différence est que l'on ne peut pas simplement définir un attribut data. Il faut obligatoirement définir une fonction renvoyant un objet contenant les propriétés et leur valeur initiale.
Démonstration :
- créer un fichier component-03.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Vue.component("myItemList", { data: function() { return { nbShow : 5 } }, props: [ 'title', 'itemList' ], template: ` <div>\ <h1>{{title}}</h1> \ <label>show</label><input v-model="nbShow"> \ <ol> \ <li v-for="(it,index) in itemList" v-if="index<nbShow">{{it.name}} : {{it.price}} gp</li> \ </ol> \ </div> ` }); |
- créer un fichier index-03.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
var app = new Vue({ el: '#mydiv', data: { items, selNums : "", selItems : [], selNames : "" }, methods: { remove : function() { this.selItems.forEach(e=> { let idx=items.indexOf(e); items.splice(idx,1); }); this.selNums=""; } }, watch:{ selNums : function(newVal, oldVal) { let lst = newVal.split(","); this.selNames = ""; this.selItems = []; lst.forEach(e => {if ((items[e-1] != undefined) && (this.selItems.indexOf(items[e-1]) == -1)) { this.selNames += items[e-1].name+" "; this.selItems.push(items[e-1]); }}); } } }) |
- créer un fichier exe-03.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="model.js"></script> <script src="components-03.js"></script> </head> <body> <div id="mydiv"> <my-item-list title="Shop proposes:" :item-list=items></my-item-list> <label for="select">#items :</label><input id="select" v-model="selNums"> <button v-if="selItems.length>0" @click="remove">remove {{selNames}}</button> </div> <script src="index-03.js"></script> </body> </html> |
- Changez la valeur du champ de saisie "Show" : plus ou moins d'item s'affichent.
- Mettez 5 dans "Show" et "9,10" dans "#item". Malgré le fait que la liste n'affiche que 5 items, le bouton fait appraître le nom des items 9 et 10. C'est normal car la gestion du bouton est séparée de celle du composant.
Remarque :
- FONDAMENTAL : quand on utilise conjointement un v-if et un v-for, ce dernier à la priorité. Le v-if va donc s'appliquer à chaque élément créé par le v-for, ce qui nous arrange bien dans ce cas pour limiter le nombre d'items affichés.
- Pour que la sélection ne puisse ce faire que sur les items affichés, il suffit que faire un composant intégrant le champ de saisie et les fonctions qui vont avec (cf. section suivante).
1.5°/ méthodes, composition de composants, envoi d'un événement du composant à son parent
- Comme pour les vue, un composant peut avoir des méthodes classiques, d'observation ou des propriétés calculées. Elles se définissent exactement de la même façon.
- Comme dit en préambule, vuejs permet d'inclure des composants dans d'autres. Pour cela, il suffit que le template du "parent" contienne des balises dont les noms sont ceux des composants "fils".
- Dans certains cas, il est utile qu'un événement se produisant dans un fils soit "remonté" à son parent. En effet, avec les balise basiques, cette remontée est automatique, mais ce n'est pas le cas avec les balises instanciant des composants.
- Pour faire cette remontée, un composant peut appeler la méthode this.$emit(nom_evenement, valeur) afin d'émettre un événement que le parent pourra catpurer avec v-on:nom_evenement, et dont la valeur sera disponible dans la variable $event.
Démonstration :
- créer un fichier component-mylist.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 |
Vue.component("myItemList", { props: [ 'itemList','nbShow' ], template: ` <div>\ <ol> \ <li v-for="(it,index) in itemList" v-if="index<nbShow">{{it.name}} : {{it.price}} gp</li> \ </ol> \ </div> `, }); |
- créer un fichier component-myshop.js et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
Vue.component("myShop", { data: function() { return { nbShow : 5, selNums : "", selItems : [], selNames : "" } }, props: [ 'title', 'itemList' ], template: ` <div>\ <h1>{{title}}</h1> \ <label>show</label><input v-model="nbShow"> \ <my-item-list :item-list="itemList" :nb-show="nbShow"></my-item-list> \ <label for="select">#items :</label><input id="select" v-model="selNums"> \ <button v-if="selItems.length>0" @click="remove">remove {{selNames}}</button> \ </div> `, methods: { remove : function() { this.selItems.forEach(e=> { let idx=items.indexOf(e); items.splice(idx,1); }); this.selNums=""; this.$emit('items-removed',this.selNames); // send an event to parent. } }, watch:{ selNums : function(newVal, oldVal) { let lst = newVal.split(","); this.selNames = ""; this.selItems = []; lst.forEach(e => {if ((e-1<this.nbShow) && (items[e-1] != undefined) && (this.selItems.indexOf(items[e-1]) == -1)) { this.selNames += items[e-1].name+" "; this.selItems.push(items[e-1]); }}); } } }); |
- créer un fichier index-04.js et copier/coller dedans :
1 2 3 4 5 6 7 |
var app = new Vue({ el: '#mydiv', data: { items, removedItems : '' } }) |
- créer un fichier exe-04.html et copier/coller dedans :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="model.js"></script> <script src="components-mylist.js"></script> <script src="components-myshop.js"></script> </head> <body> <div id="mydiv"> <my-shop title="Shop proposes:" :item-list="items" @items-removed="removedItems += $event"></my-shop> <p>items removed:{{removedItems}}</p> </div> <script src="index-04.js"></script> </body> </html> |
Remarques :
- Pour utiliser une instance de myList dans myShop, il suffit de créer une balise <my-list> avec les bons attributs dans le template de myShop.
- Les méthodes et les propriétés permettant de gérer la suppression étaient à l'origine dans la vue. Elles ont été copier/coller telles quelles dans le composant myShop.
- Le seul ajout est l'appel à this.$emit lorsque remove() est appelé. Cela permet de passer la liste des items supprimés au "parent" de l'instance composant, en l'occurence la balise <my-shop> de la page principale.
- Dans cette balise, l'événement items-removed du composant fils est capturé, et sa valeur utilisée pour modifier la propriété removeItems de la vue.
- On remarque donc que l'on peut nommer les événements émis grâce à this.$emit comme l'on veut.
1.6°/ Remarques générales
- Les points abordés ci-dessus sont justes les bases d'utilisation des composants. Il existe plein d'autres possibilités dont certaines sont expliquées dans le guide officiel vuejs : https://fr.vuejs.org/v2/guide/components.html
- La gestion des composants peut devenir un vrai casse-tête quand on utilse des composants venant d'autres développeurs. En effet, il se peut qu'ils ne prennent pas forcément des props adaptées à votre projet, ou bien qu'ils n'émettent pas les événements nécessaires, etc. Et retoucher leur code peut devenir très compliqué.