Grégory Bourguin
SysReIC - LISIC - ULCO
Vues.js - Les Composants

Mode Projet

Dans la partie précédente, nous avons travaillé en mode prototypage, c'est à dire en intégrant les scripts d'activation de Vue.js directement dans le code HTML.

Cependant, pour créer de (vraies) grosses applications single page, il est recommandé de travailler en mode projet avec npm (Node Package Manager de Node.js) et l'outil Vue CLI.

Création / Exécution / Développement

Gestion avec Vue CLI

Création

Pour créer un projet avec Vue CLI, utilisez un shell et déplacez-vous dans le répertoire qui doit contenir votre projet (ex. ~/fwf).
Utilisez ensuite la commande vue create :

vue create mon_tp

Pendant le processus de création, Vue CLI, vous demandera de choisir plusieurs options : vous pouvez dans un premier temps choisir toutes celles proposées par défaut.

Le template de projet a normalement été généré dans un dossier qui porte le nom du projet (avec un exemple de composant "HelloWorld"): il ne reste plus qu'à taper dedans :).

Exécution

Pour lancer l'exécution en mode développement, il suffit de démarrer un serveur Node.js dans le répertoire du projet.
Dans le dossier du projet (ex. ~/fwf/mon_projet), utilisez la commande :

npm run serve

Développement sous WebStorm

Si vous souhaitez utiliser WebStorm, il vous suffit maintenant de lui demander de créer un nouveau projet ("Empty Project") tout en sélectionnant le dossier qui a été généré par Vue CLI (le nom de projet de l'étape précédente) et, dans la boîte de dialogue, d'indiquer "Create from Existing Sources".

NB : il est en fait possible de créer un projet Vue CLI directement dans WebStorm (sans passer par le vue create ... dans la ligne de commande) en sélectionnant le type de projet "Vue.js" directement dans le wizard. Mais bon... personnellement j'aime bien passer par la ligne de commande :).

Structure du Projet

Comme vous pouvez le remarquer, Vue CLI a généré toute une arborescence avec de nombreux fichiers. Nous n'allons pas tous les étudier, mais voici quelques informations sur les plus importants :

  • main.js constitue le point d'entrée de l'application Vue. Il "monte" votre application sur un composant racine (id="app") défini dans index.html.
    Vous n'avez pas besoin d'y toucher.
  • index.html constitue le point d'entrée structurel. Vous n'avez (a-priori) pas besoin d'y toucher, sauf pour changer le titre et/ou le favicon.
  • App.vue constitue le véritable point d'entrée pour customiser le modèle généré par Vue CLI.
  • components/ va contenir les composants .vue qui vont être assemblés pour former l'application.

Le dossier assets est destiné à recevoir les ressources de votre applications (images, ...), node_modules contient les librairies importées avec npm, et les autres fichiers servent principalement à la configuration et au déploiement de l'application.

Application : App.vue

L'application Vue correspond au composant principal codé dans le fichier App.vue.

Sa structure contient 3 parties :

  • template : contient le code HTML qui décrit la structure de la l'application.
  • script : contient le code Vue.js décrivant la partie fonctionnelle de l'application.
  • style : contient les déclaration CSS de style pour l'application.
De fait, le fichier (composant) application généré par Vue CLI est le suivant :
App.vue
<template>
    <div id="app">
        <img alt="Vue logo" src="./assets/logo.png">
        <HelloWorld msg="Welcome to Your Vue.js App"/>  <!-- insertion du composant de démo -->
    </div>
</template>

<script>
    import HelloWorld from './components/HelloWorld.vue' // importation du composant de démo
    
    export default {
        name: 'App',
        components: {
            HelloWorld // déclaration du composant de démo
        }
    }
</script>

<style>
    #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
    }
</style>

On peut noter plusieurs points :

  • Ce template ne fait qu'intégrer un autre (sous-)composant nommé HelloWord, qui a aussi été généré par Vue CLI, et se trouve dans le fichiers HelloWorld.vue.
  • Le fait d'avoir intégré le composant dans l'application (propriété components:{ ...}) permet de l'insérer en tant que nouvelle balise <HelloWorld> dans le code du template HTML de l'application.

En suivant cet exemple, il ne nous reste plus qu'à créer nos propres composants dans le dossier ./components, à les importer et les déclarer dans l'application, puis à les insérer dans le template.

On pourra aussi en même temps supprimer HelloWorld qui ne servira plus à grand chose...

Les Composants Vue

Fichier .vue

Pour créer un nouveau composant, il faut tout d'abord créer un fichier .vue.

Dans un grand élan d'inspiration (et d'originalité) nous allons créer un composant nommé... TestComposant (!), ce qui revient à créer un fichier TestComposant.vue dans le dossier /components.

La structure basique d'un composant est (avec un peu de texte pour pouvoir le voir) :
TestComposant.vue
<template>

  <!-- ici démarre le contenu du composant -->
  <div> <!-- NB : il faut un élément 'racine'-->
    {{ message }}
  </div>

</template>

<script>
export default {
  name: "TestComposant", // le nom du composant
  data(){return{
      message: "Je suis un Composant de Test :)"
  }}
}
</script>

<style scoped>

</style>    

Dans WebStorm, il suffit de faire un click droit et "New -> Vue Component".


On a alors la structure suivante :

Template et CSS externes


Il est possible de séparer les éléments du composant en plusieurs fichiers.

Le code du template peut être placé dans un fichier externe que l'on référencera grâce à l'attribut src de la balise template au sein du fichier .vue.

Dans le fichier .vue
<template src='fichier.html'></template>

Le code CSS peut être placé dans un fichier externe que l'on référencera grâce à l'instruction @import de la balise style au sein du fichier .vue.

Dans le fichier .vue
<style scoped>
    @import "css/styles.css" ;
</style>

Intégration dans l'application

Une fois le composant créé, nous pouvons l'utiliser dans le fichier App.vue.

Pour intégrer un composant il faut :

  1. faire un import dans le script.
  2. déclarer le composant dans l'application : champ components.
  3. utiliser sa balise dans le template de l'application.
App.vue modifié :
<template>
    <div id="app">

    <test-composant></test-composant>

  </div>
</template>

<script>
import TestComposant from "@/components/TestComposant";

export default {
  name: 'App',
  components: {
    TestComposant
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>    
Résultat.

Exercice 05

Pour vérifier que vous avez tout compris à la création et l'intégration de composants, transformez l'exercice 01 en un composant nommé ex_01.vue, et intégrez le dans l'application précédente, en dessous de TestComposant.vue, en séparant les 2 avec un <hr>.

Application intégrant TestComposant et ex_01.vue.

Paramètres de Composants

Les composants Vue sont faits pour être réutilisés et, de fait, ils peuvent être paramétrés.

Déclaration

La déclaration des noms des paramètres qui peuvent être reçus par un composant est faite dans le champ props. Leur utilisation est similaire aux data.


Voici par exemple une version paramétrée (et simplifiée) de notre exercice 01 :
(Le nom du fichier est bizarre : c'est juste pour organiser mes exemples ;) )
smpl_01_fakecard.vue - 2 paramètres (name et mailServer)
<template>
  <div>
    <div class="mdl-card mdl-shadow--3dp"
         style="background: linear-gradient(45deg, #ffffff, #607D8B) ; margin: 10px 0">
      <div class="mdl-card__title mdl-card--expand">
        <div v-show="name != ''">
          <h4>
            {{ name }}
          </h4>
          <h3>
            <p>Mail : {{name}}@{{ mailServer }} </p>
          </h3>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "smpl_01_fakecard",
  props:{ // <------------ déclaration des paramètres ---------------------------
    name: String, // le type n'est pas obligatoire, mais c'est mieux de le mettre
    mailServer: String
  }
}
</script>

<style scoped>
.mdl-card{
  min-height: 70px ;
}
</style>

Utilisation

Une application (ou un autre composant) peut alors intégrer le composant en donnant des valeurs effectives à ses paramètres.

Le paramètre peut être utilisé de 2 manières :
  • le nom "simplement", pour des valeurs statiques.
  • le nom précédé d'un v-bind (ou :), pour des valeurs dynamiques (Javascript).

L'exemple suivant utilise le composant que nous venons de définir : le paramètre name reçoit une valeur dynamique, alors que mailServer reçoit une valeur statique.
App.vue
<template>
<div id="app">

  <fakecard :name="userName" mailServer="truc.fr"></fakecard>

</div>
</template>

<script>
import smpl_01_fakecard from "@/components/smpl_01_fakecard";

export default {
  name: "App",
  components:{
    fakecard: smpl_01_fakecard
  },
  data(){ return {
    userName: 'Greg'
  }},
}
</script>
Intégration paramétrée de smpl_01_fakecard dans une application

Valeur par défaut

Comme vous pouvez le voir dans la syntaxe ci-dessous, il est aussi possible de donner aux props une valeur par défaut (qui sera remplacée si une autre valeur est donnée lors de l'intégration du composant).

smpl_01_fakecard.vue - 2 paramètres (name et mailServer)
...
<script>
export default {
  name: "smpl_01_fakecard",
  props:{
    name: {
      type: String,
      default: 'unknown'
    },
    mailServer: {
      type: String,
      default: 'bidule.fr'
    }
  },
}
</script>

Exercice 06

En utilisant la classe User qui vous est fournie dans le fichier User.js, créez une application qui intègre composant nommé ex_06_UserCard.vue, sachant que ce composant est une encapsulation de la "carte de visite" créée dans la partie droite du TP01.
(cf. exemple ci-dessous).

Il y aura donc 2 fichiers : App.vue et ex_06_UserCard.vue.

Application intégrant un composant ex_06_UserCard.

NB: le composant ex_06_UserCard ne contient que la carte.
(Le titre "User Information" est au niveau de l'application, pas du composant)

Flux de Données Unidirectionnel

Les exemples ci-avant montrent qu'il est possible de passer des objets en paramètre d'un composant. Il est cependant nécessaire de comprendre que ces objets sont liés selon le principe du one-way-down binding.

One-Way-Down binding:
  • Les modifications de l'objet passé en paramètre au niveau du composant parent sont répercutées sur le composant fils qui le reçoit en paramètre.
  • PAR CONTRE, des modifications dans le composant fils sur l'objet props ne sont pas répercutées au niveau de l'objet du parent.

De ce fait, pour éviter toute ambigüité, si l'on veut que le composant fils manipule une donnée qui a été initialisée par le composant parent, il est conseillé de crée un objet data initialisé avec l'objet props.

L'exemple ci-dessous expérimente les liens et répercutions de modifications à tous les niveaux :

  • L'application gère une data globalName qui est envoyée en paramètre.
  • Le composant a une props propName dont la valeur est reçue en paramètre.
  • Le composant gère une data locale localName initialisée grâce à propName.
  • Les inputs permettent de modifier toutes ces variables et de voir les répercutions sur les autres.
smpl_03_App_owb.vue
<template>
<div id="app">

  (parent->data) globalName : <input class="input-sm" type="text" v-model="globalName"> -> {{ globalName }}

  <hr>

  <fakecard :propName="globalName"></fakecard>

</div>
</template>

<script>
import smpl_03_fakecard_owb from "@/components/samples/smpl_03_fakecard_owb";

export default {
  name: "App",
  components:{
    fakecard: smpl_03_fakecard_owb
  },
  data(){ return {
    globalName: 'Greg'
  }},
}
</script>
smpl_03_fakecard_owb.vue
<template>
  <div>
    <label>(fils->props) propName</label> :
    <input class="input-sm" type="text" v-model="propName">
    {{ propName }}

    <br>

    <label>(fils->data) localName</label> :
    <input class="input-sm" type="text" v-model="localName">
    {{ localName }}
  </div>
</template>

<script>
export default {
  name: "smpl_03_fakecard_owb",
  props:{ // <------------ déclaration des paramètres ---------------------------
    propName: String, // le type n'est pas obligatoire, mais c'est mieux de le mettre
  },
  data(){return {
    localName: this.propName
  }}
}
</script>

<style scoped>
.mdl-card{
  min-height: 70px ;
}
</style>
smpl_03_App_owb.vue : One Way Binding

Exercice 07

Complétez l'application créée dans l'exercice 06 en intégrant à l'application le formulaire qui a été créé lors du TP01 (partie gauche).

Il y aura donc 2 fichiers : App.vue et ex_06_UserCard.vue.

NB : le formulaire est pour l'instant directement intégré dans App.vue

Application intégrant le composant ex_06_UserCard.

NB: dans cette version, il n'y a pas les boutons du TP01.

Exercice 08

Il est possible d'utiliser les composants comme des balises HTML avec une boucle v-for, le paramètre envoyé au composant étant la variable de boucle.

Créez un composant ex_08_Country.vue qui sert à représenter 1 (seul) pays (tels qu'ils sont construits dans countries), puis utilisez-le dans une application qui affiche la liste des countries sous cette forme.

Résultat attendu : chaque carte de pays est une instance de ex_08_Country.
-> Respectez la mise en forme <-

Pour l'image, j'ai utilisé un Bootstrap Icon.

Évènements de Composant

Il peut parfois être nécessaire pour un composant fils de prévenir son parent d'un évènement particulier.

Vue.js permet aux composants fils de générer leurs propres évènements, et aux composants parents de les récupérer.

$emit('nom-evenement', valeur) permet la levée d'un évènement dans un composant fils.

La récupération dans le parent est réalisée par un "classique" v-on ou @.

Exemple :

  • Le composant smpl_05_TextForm.vue fournit un input de type texte et un bouton de validation. Lorsque l'utilisateur clique sur le bouton "Valider", le composant émet un évènement "text-validated" avec le contenu de l'input comme valeur.
  • Le composant parent smpl_05_App_emit.vue utilise smpl_05_TextForm.vue. Il écoute ce composant sur l'évènement "text-validated". Lorsqu'il reçoit cet évènement, le texte reçu du composant fils est affiché en tant que "Texte validé" dans le parent.
smpl_05_TextForm.vue
<template>
<div class="text-form" style="display: flex">

  <input class="form-control" v-model="textInput" placeholder="Entrez votre texte">
  <div style="width: 1em"></div>
  <button @click="validate" class="btn btn-primary">
    Valider
  </button>

</div>
</template>

<script>


export default {
  data(){ return {
    textInput: ''
  }},
  methods:{
    validate(){
      this.$emit('update:text', this.textInput)
    }
  }
}
</script>

<style scoped>
  .text-form{
    width: 300px;
  }
</style>
smpl_05_App_emit.vue
<template>
<div id="app">

  <text-form @update:text="updateText"></text-form>

  <hr>

  Texte validé : {{ text }}

</div>
</template>

<script>

import smpl_05_TextForm from "@/components/samples/smpl_05_emit/smpl_05_TextForm";

export default {
  name: "App",
  components:{
    TextForm: smpl_05_TextForm
  },
  data(){ return {
    text: ''
  }},
  methods:{
    updateText(txt){
      this.text = txt
    }
  }
}
</script>
Résultat.

Vous pourrez noter qu'à la différence de ce que nous avons fait avec les props dans l'exercice 07, l'utilisation d'un évènement de composant permet de ne transmettre des informations d'un fils à un parent que lorsque le fils l'a décidé.

Ce mécanisme est très pratique car il permet à un fils de recevoir des données d'un parent au travers d'un paramètre props, de travailler sur une version locale data, et de proposer des modifications à son parent au traver d'un $emit (au lieu de "bidouiller" une version partagée avec les problèmes d'effets de bords que cela peut entraîner).

TP 02

Réalisez la page ci-dessous.

Quelques remarques à prendre en compte :

  • Cette page ressemble au TP 01, MAIS elle DOIT cette fois être créée sous forme d'une application qui intègre les 2 composants UserCard.vue ET UserForm.vue !
    (cf. exercice 07)
  • Le bouton "Enregistrer" ne fait pas partie du composant UserForm.vue.
  • Cependant, ex_07_UserForm.vue doit être un peu modifié pour pourvoir mettre en oeuvre les mécanismes liés au bouton "Enregistrer" (activé/désactivé).
    (Vous pouvez en créer une nouvelle version appelée tp_02_UserForm.vue par ex.)
  • Testez bien tout pour voir ce qu'il faut faire !
Résultat attendu.
--> Attention à la mise en forme <--

TP 02 Extended

Dernière étape (pour l'instant ;) ), on va maintenant encapsuler ce qui a été fait en ce début de TP 02 dans un "gros" composant nommé UserInformation et lui-même l'intégrer dans une "grosse" application qui possèdera de plus une entête fournie par un nouveau composant nommé Header.vue.

Quelques remarques à prendre en compte :

  • Il y a 2 nouveaux composants à créer et intégrer : UserInformation et Header.
  • Vous remarquerez que header affiche le nick de l'utilisateur. Cependant, faites bien attention au fait que cette information n'est mise à jour que lorsqu'on l'a modifiée ET enregistrée en cliquant sur le bouton éponyme dans UserInformation !
  • UserInformation continue d'utiliser UserCard et UserForm.
  • Dans UserInformation, le bouton "Effacer" a été remplacé par "Reset" : vous noterez que son effet a changé aussi -> il réinitialise l'utilisateur avec les valeurs correspondant au dernier enregistrement !

Du fait qu'il faut parfois travailler sur une copie locale de l'utilisateur pour ensuite la valider lors d'un "Enregistrer", User.js a été complété pour permettre le clonage (méthode clone()).

Pour le composant Header, j'ai utilisé une b-navbar de Boostrap Vue.

Résultat attendu.
--> Attention à la mise en forme <--

Les Slots

La syntaxe d'insertion des composants Vue permet d'utiliser les composants comme des balises HTML qui peuvent avoir du contenu.

La balise <slot>...</slot> à l'intérieur du template d'un composant permet de d'injecter du code HTML déclaré à l'intérieur de la balise du composant au moment de son insertion.

L'exemple ci-dessous montre comment un composant PostIt peut être défini et utilisé en usilisant un slot destiné à recevoir le contenu du post-it.

PostIt.vue
<template>
<div>

  <div class="mdl-card mdl-shadow--3dp post-it" :style="'background: '+ bg">
    <slot>
      <!-- on trouvera ici le contenu de la balise -->
    </slot>
  </div>

</div>
</template>

<script>
export default {
  name: "PostIt",
  props:{
    bg: String
  }
}
</script>

<style scoped>
.post-it{
  width: 200px;
  height: 200px;

  padding: 15px;

  display: flex;
  flex-direction: column;
  justify-content: center;
}
</style>
smpl_06_App_slots.vue
<template>
<div id="app">

  <post-it bg="lightpink">
    TP FWF à rendre ! <!-- contenu inséré dans le template du composant -->
  </post-it>

  <post-it bg="yellow">

    <!-- contenu inséré dans le template du composant -->
    <h4>Courses :</h4>
    <ul>
      <li>poulet</li>
      <li>maroilles</li>
      <li>crème fraiche</li>
      <li>frites au four</li>
    </ul>

  </post-it>

  <post-it bg="lightblue">
    Anniv Greg <!-- contenu inséré dans le template du composant -->
  </post-it>

</div>
</template>

<script>


import PostIt from "@/components/samples/smpl_06_slots/PostIt";
export default {
  name: "App",
  components:{
    PostIt
  },
  data(){ return {
  }},
}
</script>

<style>
  #app{
    display: flex;

  }
  .post-it{
    margin: 5px;
  }
</style>
Résultat.

NB: il est aussi possible de créer des composants avec plusieurs slots en leur donnant des noms, de leur donner du contenu par défaut, et de leur passer des props...
Toute la documentation se trouve ici.

Cycle de vie

Les composants Vue ont un cycle de vie bien défini qu'il est intéressant de connaître, qui plus est du fait qu'il est possible d'associer des traitements à chaque étape.

Voici les différentes étapes telles que décrites dans la documentation officielle :

Exemple avec mounted

Dans l'exemple suivant, un nouvelle version de post-it (PostIt_V2) a pour particularité de calculer dynamiquement le nombre de tâches indiquées sur le post-it en se basant sur le nombre de balises li qui ont été injectées dans son slot.

Ce nombre de li ne pouvant être déterminé par le composant (lui-même) qu'une fois qu'il a effectivement été créé, on utilise un handler sur l'evènement mounted du composant.

Vous pourrez noter le recours à this.$el pour référencer l'élément HTML correspondant au composant Vue dans lequel le code se trouve.

PostIt_V2.vue
<template>
<div>
  <div class="mdl-card mdl-shadow--3dp post-it" :style="'background: '+ bg">
    <div class="mdl-card__title mdl-card--expand">
      <slot></slot>
    </div>
    <div class="mdl-card__actions mdl-card--border"
         style="background: white ; font-size: 0.8em ; text-align: center">
      Il y a {{ nbTasks }} tache<span v-if="nbTasks>1">s</span>
    </div>
  </div>

</div>
</template>

<script>
export default {
  name: "PostIt",
  props:{
    bg: String
  },
  data(){return {
    nbTasks: 0,
  }},
  mounted() { // déclenchement une fois le composant "monté"
    let monElement = this.$el // permet de récupérer l'élément HTML du composant
    // calcul dynamique du nombres de tâches
    this.nbTasks = monElement.getElementsByTagName('li').length
  }
}
</script>

<style scoped>
.post-it{
  width: 200px;
  height: 200px;

  display: flex;
  flex-direction: column;
  justify-content: center;
}
</style>
smpl_07_App_lifecycle.vue
<template>
<div id="app">

  <post-it-v2 bg="yellow">
    <ul>
      <li>truc à faire</li>
      <li>truc à faire</li>
      <li>truc à faire</li>
    </ul>
  </post-it-v2>

  <post-it-v2 bg="lightpink">
    <ul>
      <li>quelque chose</li>
      <li>quelque chose</li>
    </ul>
  </post-it-v2>

  <post-it-v2 bg="lightblue">
    <ul>
      <li>juste ça</li>
    </ul>
  </post-it-v2>

</div>
</template>

<script>

import PostIt_V2 from "@/components/samples/smpl_07_lifecycle/PostIt_V2";

export default {
  name: "App",
  components:{
    postItV2 : PostIt_V2
  },
  data(){ return {
  }},
}
</script>

<style>
  #app{
    display: flex;
  }
  .post-it{
    margin: 5px;
  }
</style>
Résultat.

TP 03

Le but de ce TP est de créer une page qui utilise 2 composants spécifiques :

  • Un composant nommé Spoiler qui, comme son nom l'indique, permet d'intégrer dans la page des composants qui affichent un bouton spoiler à la place d'un texte spécifique, ce texte n'étant visible qu'une fois qu'on a cliqué sur le bouton correspondant.
  • Un composant nommé NavItem qui génère une ancre autour d'un texte : le texte affiché est le contenu de la balise du composant (lors de son insertion) et le lien correspond au paramètre du composant nommé href.

Pour réaliser cette application, vous sont fournis :

  • Un fichier nommé template.html qui contient le template de l'application :
    CE FICHIER NE DOIT PAS ÊTRE MODIFIÉ !
  • Les images utilisées pour les fonds des entête, contenu et pied de page.

Ces fichiers sont contenus dans l'archive tp03_ressources.zip.

Pour l'intégration de template.html et la création d'un fichier CSS externe, vous pouvez vous reporter aux explications données dans le cours.

Quelques remarques à prendre en compte :

  • Le fichier template.html NE DOIT PAS ÊTRE MODIFIÉ ! (oui... j'insiste...)
    Il vous faut donc vous baser sur son contenu pour créer l'application.
  • Vous devez créer du CSS pour que tout s'affiche comme dans la démo ci-dessous.
  • Les balises spoiler sont déjà présentes dans le template.
  • Vous aurez remarqué que la liste des nav-item composant la navbar est générée dynamiquement à partir d'une variable titles.
    Cette variable DOIT elle-même être construite dynamiquement à partir des id et h1 contenus dans les articles du template de l'application.
    (pas de texte & id "en dur" dans le script de l'application SVP).
Résultat attendu.
--> Attention à la mise en forme <--

NB: Ce TP est une version Vue du TP Javascript donné ici.