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

Introduction

Qu'est-ce que Vue.js ?

Vue.js est un framework permettant de créer des interfaces utilisateur web (HTML, CSS, Javascript/TypeScript), en adoptant une approche orientée composants.

Il est en général utilisé pour créer des applications web monopage aussi appelées Single-Page Application (SPA). Les SPA se distinguent des pages web classiques dans le sens où elles permettent de fournir à l'utilisateur une interface telle qu'on la trouverait dans une application "de bureau" : les interactions avec la SPA se font sans rechargement global de la page web.

Il serait impossible (et inutile) de vous présenter ici tous les détails de Vue.js.
Ce cours n'est donc qu'une introduction aux principales fonctionnalités...
Vous pouvez trouver toute la documentation officielle sur https://v3.vuejs.org/

Pourquoi utiliser un tel Framework ?

Tout d'abord parce que les applications web modernes sont en général proposées aux utilisateurs sous forme de SPA, et Vue.js facilite grandement la création de telles applications.

D'une manière générale, Vue.js facilite la mise en place de mécanismes qui sont souvent plus lourds à réaliser en Javascript / JQuery.

Prenons un exemple : vous qui êtes maintenant des pros du Javascript (cf. super_cours), essayez de réaliser une page contenant l'exemple ci-dessous en Javascript (sans Vue.js).

Resultat

{{ name_01 }}

Nickname : {{ name_01}} The Terrible

Mail : {{name_01}}@truc.fr

Fiche de {{ name_01 != '' ? name_01 : '???' }}

Spoiler: avec Vue.js, c'est bcp plus simple :))

Les Bases du Framework

Activation version Prototypage (Apprentissage)

Comme indiqué dans la documentation il est possible d'activer Vue.js dans une page web en utilisant simplement une balise script :

<script src="https://unpkg.com/vue@next"></script>

Cette manière de travailler avec Vue.js ne doit être utilisée que pour du prototypage ou la création de composants très simples.

NB: Pour des applications conséquentes, il faut utiliser les outils de développement tels Vue CLI (que nous verrons plus loin dans ce cours).

Application Vue

La création d'application Vue.js au sein d'une page web contient 2 parties principales :

  • Une balise quelconque (ex. un div avec un id (unique!) contenant des expressions qui seront utilisée par le framework pour faire du rendu dynamique(Vue va se charger de lier des données au DOM de la balise)
  • Une balise script contenant les propriétés et méthodes de l'application.

Data & Rendu déclaratif : {{ ... }}

Grâce à la notation {{ objet }}, Vue permet de créer des éléments HTML complexes dynamiquement liés aux données de l'application.

<!-- Balise racine de l'application : --> 
<!-- le DOM va être lié à Vue.js pour faire du rendu dynamique -->
<div id="app-01">
    {{ message_01 }}  <!-- référence à la propriété message_01 : dynamique !!! -->
    <br>
    {{ message_02 }}  <!-- référence à la propriété message_02 : dynamique !!! -->
</div>

<script> // Script déclarant les données et méthodes de l'application -->

    // définitions
    const Application_01 = {
      data() {
        return {
          message_01: 'Vue.js vous dit bonjour.', // les propriétés sont séparées par ','
          message_02: 'C\'est coooool !!!'
        }
      }
    }
    
    // instanciation de l'application, et liaison avec la balise racine de l'application
    Vue.createApp(Application_01).mount('#app-01')
</script>
Resultat
{{ message_01 }}
{{ message_02 }}

Génération de code HTML : v-html

La directive v-html="code_html", permet d'injecter du code HTML dans les balises.

<!-- Balise racine de l'application dont le DOM va être lié à Vue.js pour faire du rendu dynamique -->
<div id="app-01-bis">
    <div v-html="message"></div>
</div>

<script> // Script déclarant les données et méthodes de l'application -->

    // définitions
    const Application_01_bis = {
      data() {
        return {
          message: '<b style="color: blue">Vue.js vous dit bonjour !!!</b>'
        }
      }
    }
    
    // instanciation de l'application, et liaison avec la balise racine de l'application
    Vue.createApp(Application_01_bis).mount('#app-01-bis')
</script>
Resultat

Méthodes

Les applications Vue peuvent déclarer et utiliser des méthodes qui contiennent des instruction écrites en Javascript. Les méthodes peuvent être appelées dans les instructions de rendu ou par d'autres méthodes (grâce à this).

Nous verrons ci-après (cf. Évènements) que les méthodes peuvent aussi servir de handler et être déclenchées par des évènements. Elles permettent au besoin de manipuler les propriétés décritss dans la partie data, ce qui a (généralement) un effet direct sur le rendu de l'application.

Ex. appel de méthodes dans le rendu
<div id="app-methods">
    <div>
        {{ mirror(message) }}
    </div>
</div>

<script>
    
    const Application_Methods = {
      data() {
        return {
          message: 'Hello World'
        }
      },
      methods: {
          decorate(txt){
             return "--> " + txt + " <--" 
          },
          mirror(txt){
              txt = txt.split('').reverse().join('')
              txt = this.decorate(txt)
              return txt
          }
      }
    }
    Vue.createApp(Application_Methods).mount('#app-methods')
</script>
Resultat
{{ mirror(message) }}

Évènements : v-on: et @

Comme en Javascript, il est possible d'enregistrer des écouteurs d'évènements sur les gestionnaires associés des balises HTML.

L'enregistrement dans un gestionnaire d'évènements est réalisée grâce à la directive v-on:event=handler qui doit être insérée directement dans la balise de l'objet DOM visé.

event représente le type d'évènement qu'on souhaite écouter (ex. click).
handler représente la méthode de l'application qui sera déclenchée.

Écouter un click
<div id="app-02">
    Prénom : {{ firstName }}<br>
    Nom : {{ lastName }} <br><br>
    <button v-on:click="remplirTexte">Remplir</button>
</div>

<script>
    const App_02 = {
        data(){ return{
            firstName: '',
            lastName: ''
        }},
        methods: {
            remplirTexte(){
                this.firstName = 'Gregory'
                this.lastName = 'Bourguin'.toUpperCase() // pour le fun
            }
        }
    }
    
    Vue.createApp(App_02).mount('#app-02')
</script>
Resultat
Prénom : {{ firstName }}
Nom : {{ lastName }}

Il est aussi possible d'écouter d'autres types d'évènements, de créer des handlers paramétrés, et au besoin de récupérer l'objet évènement qui a été généré (comme en Javascript).

Pour les évènements du DOM, on peut remplacer le v-on: par un @ (ex. @click="handler")

Écouter des évènements, passer des paramètres, récupérer l'event, ...
<div id="app-03">
    Prénom : {{ firstName }}<br>
    Nom : {{ lastName }} <br><br>
    <button @click="remplirTexte" 
            @mouseenter="changerTexteBouton('Allez clique !')"
            @mouseout="changerTexteBouton('Reviiiiennnnnsss !', $event)"
            >{{ txt_du_bouton }}
    </button> (clicks : {{ nb_clicks }})
</div>

<script>
    const App_03 = {
        data(){ return{
            firstName: '',
            lastName: '',
            txt_du_bouton: 'Remplir',
            nb_clicks: 0
        }},
        methods: {
            remplirTexte(){
                this.firstName = 'Gregory'
                this.lastName = 'Bourguin'.toUpperCase() // pour le fun
                this.nb_clicks++
            },
            changerTexteBouton(msg, event){
                this.txt_du_bouton = msg
                
                // récupération de la cible de l'évènement et modification du style
                event.target.style.color = 'red'
            },
        }
    }
    
    Vue.createApp(App_03).mount('#app-03')
</script>
Resultat
Prénom : {{ firstName }}
Nom : {{ lastName }}

(clicks : {{ nb_clicks }})

Conditions : v-if

v-if="condition" permet d'indiquer à Vue si un élément du DOM doit être présent dans la page ou non. L'élément sera présent uniquement quand la condition est vérifiée.

Bien entendu, tout cela est dynamique et la condition peut être dépendante des données de l'application... dont la valeur peut évoluer au cours du temps...

Un simple v-if
<div id="app-04" style="height: 5em">

    <button @click="toggleVisibility">Visible : {{ visible }} </button>

    <div v-if="visible" style="margin: 10px 0 ; background: greenyellow">
        Ce div risque de disparaître...
    </div>
    
</div>

<script>
    const App_04 = {
        data(){ return{
            visible: true
        }},
        methods: {
            toggleVisibility(){
                this.visible = !this.visible
            }
        }
    }
    Vue.createApp(App_04).mount('#app-04')
</script>
Resultat
Ce div risque de disparaître...

Forms Inputs & Models : v-model

La directive v-model="property" permet se lier l'input d'un formulaire à une propriété de l'application.

Cette liaison est bi-directionnelle dans le sens où une modification de la valeur de l'input sera répercutée sur la propriété (et donc ses autres apparitions dans la page seront mises à jour), et une modification de la propriété sera répercutée dur le champ qui l'utilise.

Une propriété liée à un champ de type texte
<div id="app-05" style="display: flex ; flex-direction: column">

    <input type="text" v-model="txt" placeholder="votre texte ici">
    
    <div style="margin: 10px 0 ; border-radius: 3px ; padding: 5px ; background: black ; color: greenyellow">
        <fieldset>
            <legend style="color: greenyellow">StyliZator</legend>
            <table>
                <tr>
                    <td>original   </td><td>&nbsp; &gt; </td>
                    <td>&nbsp; {{ txt }}</td>
                </tr>
                <tr>
                    <td>upper-case </td><td>&nbsp; &gt; </td>
                    <td>&nbsp; {{ txt.toUpperCase() }}</td>
                </tr>
                <tr>
                    <td>lower-case </td><td>&nbsp; &gt; </td>
                    <td>&nbsp; {{ txt.toLowerCase() }}</td>
                </tr>
                <tr><td>reverse   </td><td>&nbsp; &gt; </td>
                    <td>&nbsp; {{ txt.split('').reverse().join('') }}</td>
                </tr>
            </table>
        </fieldset>
    </div>
    
    <div><button @click="clear">Clear</button></div>
    
</div>

<script>
    const App_05 = {
        data(){ return{
            txt: ''
        }},
        methods: {
            clear(){
                this.txt = ''
            }
        }
    }
    Vue.createApp(App_05).mount('#app-05')
</script>
Resultat
StyliZator
original   >   {{ txt }}
upper-case   >   {{ txt.toUpperCase() }}
lower-case   >   {{ txt.toLowerCase() }}
reverse   >   {{ txt.split('').reverse().join('') }}

Il est bien entendu possible de lier des propriétés à des champs de tous types comme les textarea, checkbox, radio, select, etc.

<div id="app-06" style="height: 5em">
    <form style="display: flex">
        <label class="form-check-label" for="app-06-check">Visible</label>&nbsp;
        
        <input v-model="visible" type="checkbox" class="form-check-input" id="app-06-check"> 
    </form> 
    
    <div v-if="visible" style="margin: 10px 0 ; background: greenyellow">
        Ce div risque de disparaître...
    </div>
    
</div>

<script>
    const App_06 = {
        data(){ return{
            visible: true
        }}
    }
    Vue.createApp(App_06).mount('#app-06')
</script>
Resultat
 
Ce div risque de disparaître...

Vous pouvez trouver la documentation et des exemples pour tous les type d'inputs à https://v3.vuejs.org/guide/forms.html#basic-usage

À propos de v-model

Si v-model est la plupart du temps suffisant pour lier un input à une variable, il peut aussi être intéressant de noter que v-model correspond en réalité à une paire liant un évènement et une valeur grâce à v-on & v-bind.

Pour donner un exemple, les 2 extraits de code ci-dessous sont écrits différemment mais font exactement la même chose sur un input de type text.

Exemple avec v-model
<div id="input-v-model">
    <input v-model="texte"> {{ texte }}
</div>
<script>
    Vue.createApp({
        data(){return{
            texte: 'TEST'
        }}
    }).mount('#input-v-model')
</script>
Resultat
{{ texte }}
Le même avec v-on et v-bind
<div id="input-v-on-bind">
    <input @input="texte = $event.target.value" :value="texte"> {{ texte }}
</div>
<script>
    Vue.createApp({
        data(){return{
            texte: 'TEST'
        }}
    }).mount('#input-v-on-bind')
</script>
Resultat
{{ texte }}

Exercice 1

Vous avez maintenant tout ce qu'il faut pour réaliser par vous même l'exemple du tout début du cours, mais cette fois-ci en Vue.js :)

Boucles : v-for

v-for='(obj, index) in list' est une directive qui permet de générer une liste d'éléments HTML à partid des données contenues dans une liste d'objets Javascript.

Encore une fois, la génération des éléments est dynamique, c'est à dire que toute modification à la list sera immédiatement répercutée.

NB : on peut aussi ne pas utiliser l'index avec un simple : v-for='obj in list'

<div id="app-07">
    Weather Forecasts : 
    <ul>
        <li v-for="(fc, index) in forecasts" :key="fc.name">
            (day {{index+1}}) <b>{{ fc.day }} :</b> <i>{{ fc.weather}}</i>
        </li>
    </ul>
    <button class="btn btn-primary" @click="calm_down">calm down</button>
</div>

<script>
// Une autre façon de créer l'application Vue.js

const weather_forecasts = [
    { day: 'monday', weather: 'sun'},
    { day: 'tuesday', weather: 'clouds'},
    { day: 'wednesday', weather: 'rain'},
    { day: 'friday', weather: 'storm'},
    { day: 'thursday', weather: 'ice'},
    { day: 'saturday', weather: 'snow'},
    { day: 'sunday', weather: 'armageddon'},
]

Vue.createApp({
    data(){ return {
        forecasts: weather_forecasts
    }},
    methods: {
        calm_down(){
            console.log(this.forecasts.length)
            this.forecasts[this.forecasts.length-1].weather = 'SUUUUNNNNNNYYYY !'
        }
    }
}).mount('#app-07')
 
</script>
Resultat
Weather Forecasts :
  • (day {{index+1}}) {{ fc.day }} : {{ fc.weather}}

Exercice 2

En utilisant le fichier sample_countries.js, réalisez la page ci-dessous.
NB: vous pouvez importer l'objet countries en commençant votre script comme ci-dessous.
(Il faut un serveur web pour qu'il n'y ait pas d'erreur CORS à l'exécution.)
<script type="module">
    import { countries } from "../js/sample_countries.js"; // NB: mettre votre chemin
        
    // la suite ici
</script>

Il est possible démarrer un serveur local (ici sur le port 8000) dans une console avec :
- En Python (3) : python -m http.server 8000
- En PHP : php -S localhost:8000

Attributs des balises : v-bind et :

La directive v-bind:attribute="value" permet de lier des objets aux attributs (e.g. id, href, style, class...) des balises HTML de l'application.

v-bind: peut aussi être simplement remplacé par :

Liaison sur la valeur 'globale' de l'attribut

Ex. sur l'attribut style
<div id="app-08">
    <fieldset>
        <legend>Global Style Tester</legend>
        <div style="display: flex ; align-items: center">
            <input v-model="custom_style" 
                type="text" class="form-control input-sm"
                placeholder="Enter style here"
            >
            &nbsp;
            <button @click="custom_style = ''" class="btn btn-warning">Clear</button>
        </div>
    </fieldset>
    <br>
    <div v-bind:style="custom_style">Un div qui a du style grâce à "v-bind"</i></div>
    <br>
    <div :style="custom_style">Un div qui a du style grâce à ":"</div>
</div>

<script>

Vue.createApp({
    data(){ return {
        custom_style: 'padding: 10px ; background: black ; color: greenyellow ;'
    }},
}).mount('#app-08')
 
</script>
Resultat
Global Style Tester
 

Un div qui a du style grâce à "v-bind"

Un div qui a du style grâce à ":"

Liaison sur les propriétés de l'attribut

Ex. sur l'attribut style
<div id="app-09">
    <fieldset>
        <legend>Props Style Tester</legend>
            <label for="app-09-bg">Background</label>
            <input v-model="custom_bg"
                id="app-09-bg" 
                type="text" class="form-control input-sm"
            >
            <label for="app-09-color">Color</label>
            <input v-model="custom_color"
                id="app-09-color" 
                type="text" class="form-control input-sm"
            >
    </fieldset>
    <br>
    
    <div :style="{ background: custom_bg, color: custom_color }" 
         class="form-control">
         Un div qui a du style ! 
    </div>
</div>

<script>

Vue.createApp({
    data(){ return {
        custom_bg: 'black', 
        custom_color: 'greenyellow'
    }},
}).mount('#app-09')
 
</script>
Resultat
Props Style Tester

Un div qui a du style !

Classes des balises : :class

Comme nous l'avons vu au point précédent, il est possible de lier l'attribut class à un objet (:class est le raccourcis pour v-bind:class). Cependant, Vue permet d'associer chaque classe à une variable ou expression qui conditionnera son application à l'élément visé.

:class="{ classe1: cond1, ..., classN: condN }" rend l'application d'une classeX dépendante de l'expression booléenne condX.

<style>
    .app-10-basic{
        background: black;
        height: auto;
    }
    .app-10-higlight{
        color: greenyellow;
        font-weight: bolder;
    }
    .biiig{
        font-size: 2em;
        text-transform: capitalize;
    }
</style>

<div id="app-10">
    <fieldset>
        <legend>Class Tester</legend>
        <div style="display: flex ; flex-direction: row ;">
            <div class="form-group">
                <label class="form-check-label" for="app-10-c1">Hilighted</label>&nbsp;
                <input v-model="hilighted" 
                       type="checkbox" class="form-check-input" id="app-10-c1">
            </div> 
            <div style="width: 50px"></div>
            <div class="form-group">
                <label class="form-check-label" for="app-10-c2">Big</label>&nbsp;
                <input v-model="big" 
                       type="checkbox" class="form-check-input" id="app-10-c2">
            </div>
        </div>
    </fieldset>
    
    // Les valeurs de hilighted et big déterminent l'application des classes CSS
    <div :class="{ 'app-10-higlight': hilighted, biiig: big }"   
         class="app-10-basic form-control">
         Un div qui a du style ! 
    </div>
    
</div>

<script>    
Vue.createApp({
    data(){ return {
        hilighted: true, 
        big: false, 
    }},
}).mount('#app-10')
 
</script>
Resultat
Class Tester
 
 
// Les valeurs de hilighted et big déterminent l'application des classes CSS
Un div qui a du style !

Propriétés Calculées : computed

L'application Vue peut parfois avoir besoin d'afficher des propriétés dont la valeur résulte d'un calcul complexe (trop complexe pour être directement écrit dans les {{ ... }}.

Le champ computed permet de créer des propriétés d'application qui seront calculées (et constamment mises à jour) dynamiquement.

Les propriétés computed ressemblent fortement à des méthodes qui seraient appelées dans le code HTML. Il existe cependant une différence : les méthodes sont exécutées à chaque apparition dans le code, alors que les résultats des propriétés calculées sont mises en cache et ne sont recalculés que lorsque leurs dépendances sont modifiées.

<div id="app-computed">
    <div class="form-group">
        <label for="app-computed-msg">Message</label>
        <input v-model="message" type="text" class="form-control" id="app-computed-msg">
    </div>
    <div class="form-group">
        <label for="app-computed-limit">Short message limit</label>
        <input v-model="short_limit" type="text" class="form-control" id="app-computed-limit">
    </div>
    
    <div class="console_output">
        <div>Message : {{ message }}</div>
        <div>Type : {{ size }}</div>
    </div>
</div>

<script>
Vue.createApp({
    data(){return{
        message: 'Hello World',
        short_limit: 5
    }},
    computed: {
        size(){
            if (this.message.length < this.short_limit){
                return 'short'
            }else{
                return 'long'
            }
            
            // Ok ... on aurait pu faire ;)
            // return this.message.length < this.short_limit ? 'short' : 'long'
        }
    }
}).mount("#app-computed")
</script>
Resultat
Message : {{ message }}
Type : {{ size }}

TP 01

Réalisez la page ci-dessous.

Quelques remarques à prendre en compte :

  • Testez bien tout pour voir ce qu'il faut faire !
  • J'ai bloqué le pays à "France" (dans un input de type texte) pour être cohérent avec le format du Téléphone...
  • Mettez bien en place la gestion des erreurs (couleur rouge + messages sous les inputs) !
  • Mettez bien en place le formatage automatique du téléphone dans la carte : 1er chiffre tt seul, puis des paires.
  • Vous remarquerez que le bouton "Enregistrer" est désactivé tant que tous les champs ne sont pas correctement remplis.
  • Lors du click sur "Enregistrer", j'ai utilisé une boite de dialogue Modal de Bootstrap.
  • J'ai utilisé .replaceAll(...), .trim(), .substring(...) et RegExp pour faire les vérifications / transformations.
  • J'ai "abusé" des propriétés "computed".
  • Vous remarquerez que l'input du téléphone empêche d'entrer des espace...
    Vous n'êtes pas obligés d'en faire de même. Cependant, si vous voulez essayer: dans l'input, j'ai remplacé le v-model par une paire @input="..." (un handler pour l'évènement), et un :value="..." pour la valeur affichée dans l'input.

Fichiers : sample_countries.js, baseline_face_black_48dp.png

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

Les Observateurs : watch

Il est parfois nécessaire de créer des traitements complexes qui doivent être déclenchés uniquement lorsque la valeur d'une (ou plusieurs) autre(s) propriété(s) a(ont) changé.

NB: c'est ce que font les propriétés computed, mais nous parlons ici de traitements plus complexes qu'un "simple" calcul de valeur.

Le champ watch permet de créer des observateurs de propriétés qui ne seront déclenchés que lorsque la valeur de la propriété observée a changé.

Dans l'exemple suivant, les watchers sont utilisés pour :

  • Rendre les propriétés kg et g interdépendantes, c.a.d. que la modification de l'une, entraine la mise à jour de l'autre : il ne s'agir donc pas de faire un computed "classique" car la dépendance qui nous intéresse ici est bidirectionnelle.
    (NB: ...bon ...il serait en fait possible de le faire avec un computed + des accesseurs, mais ce n'est pas le sujet ici ;) ).
  • Déclencher l'apparition d'un message (uniquement) lorsque la valeur de la propriété conclusion (qui est elle-même computed !) a changé.
<div id="app-watch">

    <div class="form-group">
        <label>g</label>
        <input v-model="g" type="number" class="form-control">
    </div>

    <div class="form-group">
        <label>Kg</label>
        <input v-model="kg" type="number" class="form-control">
    </div>

    <div style="height: 6em ; border: 1px solid lightgrey">
        <div id="msg" style="font-size: 4em ; display: none">
            {{ conclusion }}
        </div>
    </div>

</div>

<script>
    Vue.createApp({
        data(){return{
            g: '',
            kg: ''
        }},
        computed:{
          conclusion(){
              return this.kg < 100 ? "Trop léger..." : "Wow, c'est lourd !"
          }
        },
        watch: {
            kg: function (newValue, oldValue){ // un watcher
                this.g = newValue * 1000
            },
            g: function (newValue){ // une autre manière de l'écrire
                this.kg = newValue/1000
            },
            conclusion(newValue, oldValue) { // ... et encore une autre
                console.log("changé")
                this.flashMsg()
            }
        },
        methods: {
            flashMsg(){
                $("#msg").fadeIn(500, function (){
                    $("#msg").fadeOut(500)
                })
            }
        }
    }).mount("#app-watch")
</script>

<style>
    .form-group{
        display: flex;
        align-items: center;
    }
    label{
        margin-right: 1em;
    }
</style>
NB: un message apparaît à chaque fois qu'on passe la barre des 100Kg