Vue.js 2

inspiré de la documentation officielle

 

 

https://interactly.glitch.me/

Introduction

Framework JavaScript pour le développement d'application web

 

Simplifie le développement d'interface dynamique

 

Populaire : Adobe, GitLab, ...

Historique

Juil 2013 - Premier commit par Evan You (@Google)

 

Fév 2014 - Version 0.9

 

Oct 2015 - Version 1.0

 

Sep 2016 - Version 2.0

 

Sep 2020 - Version 3.0

Pourquoi Vue.js ?

Réelle plus value

 

Puissant

 

Léger

 

Open source

Populaire

 

Communauté active

 

Maintenu

 

Bien documenté

 

 

Grosso modo, que fait Vue ?

Permet de séparer la vue, le modèle de données et la logique applicative

  • Gère à notre place le rendu de la vue, et la mise à jour des données
  • Gère à notre place le binding événementiel

Êtes vous prêts pour découvrir Vue ?

 

Get started

Intégration à la volée de Vue

Version de développement
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

Version de production
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello world !'
  }
})
Hello world !

Réactivité: premier contact

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello world !'
  }
})
Hello world !

Réactivité premier contact

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello world !'
  }
})
Vue is reactive !
> app.message = "Vue is reactive";

LIVE

Rendu déclaratif

<div id="app">
  <img v-bind:src="url" />
</div>
var app = new Vue({
  el: '#app',
  data: {
    url: "https:// [...] "
  }
})

LIVE

Conditions

<div id="app">
  <p v-if="seen">Je suis visible</p>
</div>
var app = new Vue({
  el: '#app',
  data: {
    seen: true
  }
})

LIVE

Boucles

<div id="app">
  <ol>
    <li v-for="verb in verbs">
      {{ verb.text }}
    </li>
  </ol>
</div>
var app = new Vue({
  el: '#app',
  data: {
    verbs: [
      { text: 'Veni' },
      { text: 'Vidi' },
      { text: 'Vici' }
    ]
  }
})

LIVE

Entrées utilisateurs

<div id="app">
  <p>{{ message }}</p>
  <button v-on:click="capitalize">
    Capitaliser !
  </button>
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: "Hello world !"
  },
  methods: {
    capitalize() {
      this.message = this.message.toUpperCase();
    }
  }
});

Binding bidirectionnel

<div id="app">
  <p>{{ message }}</p>
  <input v-model="message" />
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: "Hello world !"
  }
});

Composants

 

Déclarer un composant

<div id="app">
  <bold-italic></bold-italic>
</div>
Vue.component("bold-italic", {
  template: "<b><i>Gratalic</i></b>"
});

var app = new Vue({
  el: "#app",
  data: {}
});

Les props

<div id="app">
  <bold-italic v-bind:content="text">
  </bold-italic>
</div>
Vue.component("bold-italic", {
  template: "<b><i>{{ content }}</i></b>",
  props: ["content"]
});

var app = new Vue({
  el: "#app",
  data: {
    text: "Ninja !"
  }
});

Boucles et props

<div id="app">
  <bold-italic
    v-for="fruit in fruits"
    v-bind:content="fruit.name"
    v-bind:key="fruit.id"
    ></bold-italic>
</div>
Vue.component("bold-italic", {
  template: "<b><i>{{ content }}</i></b>",
  props: ["content"]
});

var app = new Vue({
  el: "#app",
  data: {
    fruits: [
      { id: 0, name: "Apple" },
      { id: 1, name: "Pear" }
    ]
  }
});

Pour utiliser Vue, il faut

 

La version de production de Vue

 

Plusieurs instances de Vue peuvent tourner sur une même page

 

La réactivité, ça veut dire

 

L'attribut v-model

 

Exercice

Créer une micro application permettant d'ajouter des éléments à une liste à l'aide d'un champ texte et d'un bouton.

 

Web IDE pré configuré + Aide : https://bit.ly/3kksZvz

Instance de Vue

Création d'une instance

Instance racine
└─ TodoList
   ├─ TodoItem
   │  ├─ TodoButtonDelete
   │  └─ TodoButtonEdit
   └─ TodoListFooter
      ├─ TodosButtonClear
      └─ TodoListStatistics
var vm = new Vue({
  // options
})

Réactivité de l'état local

 

// Notre objet de données
var data = { a: 1 }

// L'objet est ajouté à une instance de Vue
var vm = new Vue({
  data: data
})

// Récupérer la propriété depuis l'instance
// retourne celle des données originales
vm.a == data.a // => true

// assigner la propriété à une instance
// affecte également la donnée originale
vm.a = 2
data.a // => 2

// ... et vice-versa
data.a = 3
vm.a // => 3

Réactivité : une fois pour toute

 

// Notre objet de données
var data = { a: 1 }

// L'objet est ajouté à une instance de Vue
var vm = new Vue({
  data: data
})

// vm.a est réactive : ses mutations engendreront des mises à jour dans la vue #MVVM
vm.a

// Si on rajoute une propriété à l'état en cours de route, elle ne sera pas réactive : l'état local est ajouté au système de réactivité uniquement lors de l'instanciation de Vue
vm.b = 3

Propriétés et méthodes de Vue

 

// Notre objet de données
var data = { a: 1 }

// L'objet est ajouté à une instance de Vue
var vm = new Vue({
  el: '#example',
  data: data
})


// L'instance de Vue expose de nombreuses méthodes et propriétés utiles

vm.$data === data // => true
vm.$el === document.getElementById('example') // => true

// $watch est une méthode de l'instance
vm.$watch('a', function (newVal, oldVal) {
  // cette fonction de rappel sera appelée quand `vm.a` changera
})

Cycle de vie de Vue

 

Cycle de vie de Vue

 

Cycle de vie de Vue

 

Cycle de vie de Vue

 

Hooks de Vue

 

new Vue({
  data: {
    a: 1
  },
  created() {
    // `this` est une référence à l'instance de vm
    console.log('a is: ' + this.a)
  }
})
// => "a is: 1"

Syntaxe de template

Les templates dans Vue

Syntaxe basée sur HTML, validité requise

 

Les templates sont compilés pour être plus performants

 

Vue supporte JSX

Interpolations

<span>Message: {{ message }}</span>
<span v-once>
  Ce message ne changera plus: {{ message }}
</span>

Interprétation du HTML

<div>Intepreté comme du texte : {{ rawHTML }}</div>
<div>
  Intepreté comme du HTML : 
  <span v-html="rawHtml"></span> <!-- /!\ XSS -->
</div>

Attributs

<div v-bind:id="dynamicId"></div>
<button v-bind:disabled="isButtonDisabled">Button</button>
// Cas des attributs booléan : si vaut null, undefined, ou false, l'attribut disabled n'est pas ajouté à l'élément

Expression JavaScript

{{ number + 1 }}

{{ ok ? 'OUI' : 'NON' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

Non-Expression JavaScript

<!--Cas d'erreur :-->

<!--Ceci est une déclaration, pas une expression -->
{{ var a = 1 }}

<!--Le contrôle de flux ne marchera pas non plus, utilisez des expressions ternaires -->
{{ if (ok) { return message } }}

Accès global restreint

<!-- Cas d'erreur -->
{{ uneVariableGlobaleRandom }}
<!-- Succès -->
{{ Math.min(5, maVariableLocale) }}

Les directives

v-for

 

v-if

 

v-else-if

 

v-else

 

v-*

Les directives avec arguments

<a v-bind:href="url"> ... </a>
<a v-on:click="doSomething"> ... </a>

Les arguments dynamiques

<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>

Vue 2.6

Text

Donné au type string ou null, sinon erreur

Modificateurs

<form v-on:submit.prevent="onSubmit"> ... </form>

Abréviation de v-bind

<!-- syntaxe complète -->
<a v-bind:href="url"> ... </a>

<!-- abréviation -->
<a :href="url"> ... </a>

<!-- abréviation avec argument dynamique (2.6.0+) -->
<a :[key]="url"> ... </a>

Abréviation de v-on

<!-- syntaxe complète -->
<a v-on:click="doSomething"> ... </a>

<!-- abréviation -->
<a @click="doSomething"> ... </a>

<!-- abréviation avec argument dynamique (2.6.0+) -->
<a @[event]="doSomething"> ... </a>

L'état local

 

Les données de l'état local sont ajoutées au système de réactivité

 

L'instance de Vue peut contenir

 

Vue est "monté"

 

Dans un template, on peut mettre

entre {{ double accolade }}

 

Les templates de Vue

 

v-bind

 

v-on

 

v-bind:title et v-on:input

peuvent être écrits

 

Propriétés calculées et observateurs

Problématiques

<div>
  {{ message.split('').reverse().join('') }} <!-- #1 Simplicité -->
</div>

...

<div>
  {{ message.split('').reverse().join('') }} <!-- #2 Performance -->
</div>

...

<div>
  {{ message.split('').reverse().join('') }} <!-- #3 Répétition -->
</div>

Propriété calculée

<div id="app">
  <p>
    Message original : {{ message }}
  </p>
  <p>
    Message inversé calculé : {{ reversedMessage }}
  </p>
</div>
var vm = new Vue({
  el: '#app',
  data: {
    message: 'Bonjour'
  },
  computed: {
    // un accesseur (getter) calculé
    reversedMessage: function () {
      // `this` pointe sur l'instance vm
      return this.message
        .split('')
        .reverse()
        .join('')
    }
  }
})

LIVE

methods vs computed

computed: {
  now: function () {
    return Date.now()
  }
}
methods: {
  reverseMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

Différence : computed met en cache son résultat

watchers vs computed

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

Mutateur calculé

vm.fullName = 'John Doe';

// =>

vm.firstName; // 'John'

vm.lastName; // 'Doe'
computed: {
  fullName: {
    // accesseur
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
      
    // mutateur
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

Classe et style

v-bind:class + syntaxe objet

<div v-bind:class="{ active: isActive }"></div>

v-bind:class + syntaxe objet

<div :class="{ active: isActive }"></div>

v-bind:class + syntaxe objet

<div 
  class="static" 
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

v-bind:class + syntaxe objet

<div 
  class="static" 
  :class="{ active: isActive, 'text-danger': hasError }"
></div>
data: {
  isActive: true,
  hasError: false
}
<!-- Résultat -->
<div 
  class="static active">
</div>

v-bind:class + syntaxe objet

<div :class="classObject"></div>
data: {
  classObject: {
    active: true,
    'text-danger': false
  }
}
<!-- Résultat -->
<div 
  class="active">
</div>

v-bind:class + syntaxe objet

<div :class="classObject"></div>
data: {
  isActive: true,
  error: null
},
computed: {
  classObject() {
      return { 
        active: this.isActive && !this.error
      }
  }
}
<!-- Résultat -->
<div 
  class="active">
</div>

v-bind:class + syntaxe tableau

<div v-bind:class="['staticClass', dynamicClass]"></div>
data: {
  dynamicClass: 'foo',
}
<!-- Résultat -->
<div 
  class="staticClass foo">
</div>

v-bind:class + syntaxe tableau

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
<div v-bind:class="[{ active: isActive }, errorClass]"></div>

v-bind:class sur composant

Vue.component('my-component', {
  template: '<p class="foo bar">Hi</p>'
})
<!-- Résultat -->
<p class="foo bar baz boo">Hi</p>
<my-component 
  class="baz boo"
></my-component>

v-bind:style + syntaxe objet

data: {
  activeColor: 'red',
  fontSize: 30
}
<!-- Résultat -->
<div style="color: red; font-size: 30px;"></div>
<div 
  v-bind:style="{ 
    color: activeColor, 
    fontSize: fontSize + 'px' 
  }"
></div>

v-bind:style + syntaxe tableau

<div v-bind:style="[baseStyles, overridingStyles]"></div>

v-bind:style + préfixes CSS

<div v-bind:style="{transform: 'translate(120px, 50%)'}"></div>

Préfixage automatique selon le naviguateur

Rendu conditionnel

v-if

<h1 v-if="awesome">Vue est extraordinaire !</h1>

v-else

<h1 v-if="awesome">Vue est extraordinaire !</h1>
<h1 v-else>Oh non 😢</h1>

v-else-if

<h1 v-if="awesome">Vue est extraordinaire !</h1>
<h1 v-else-if="nice">Vue est sympa.</h1>
<h1 v-else>Oh non 😢</h1>

v-if sur un groupe d'éléments

<template v-if="ok">
  <h1>Titre</h1>
  <p>Paragraphe 1</p>
  <p>Paragraphe 2</p>
</template>

v-if avec v-for

<div v-if="user.connected" v-for="user in users">
  {{ user.pseudo }}
</div>

v-show

<h1 v-show="ok">Bonjour !</h1>

v-show effectue toujours le rendu :

il permute la propriété CSS display

key

<template v-if="loginType === 'username'">
  <label>Nom d'utilisateur</label>
  <input placeholder="Entrez votre nom d'utilisateur">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Entrez votre adresse email">
</template>

Contrôler la ré-utilisabilité d'un élément dans le rendu

key

<template v-if="loginType === 'username'">
  <label>Nom d'utilisateur</label>
  <input placeholder="Entrez votre nom d'utilisateur" key="name">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Entrez votre adresse email" key="email">
</template>

Contrôler la ré-utilisabilité d'un élément dans le rendu

Quelles classes seront appliquées ?

 

<div class="foo" :class="{ bar: displayBar, baz, qux: false }"></div>
data: {
  foo: false,
  bar: true,
  displayBar: false,
  baz: false,
  qux: true
}

Quels styles seront appliqués ?

 

<div :style="{ backgroundColor, color: activeColor }"></div>
data: {
  activeColor: 'red',
  color: 'blue',
  fontSize: 30,
  backgroundColor: 'white'
}

La directive pour faire "else if"

 

À propos de l'utilisation de v-if et v-for sur le même élément

 

La différence entre v-if et v-show

 

La directive key

 

Rendu de liste

v-for sur tableau

    <ul id="app">
      <li v-for="item in items">
        {{ item.message }}
      </li>
    </ul>
var app = new Vue({
  el: "#app",
  data: {
    items: [
      { message: "Foo" }, 
      { message: "Bar" }
    ]
  }
});

v-for indexé

<ul id="app">
  <li v-for="(item, index) in items">
    #{{index}} {{ item.message }}
  </li>
</ul>
var app = new Vue({
  el: "#app",
  data: {
    items: [
      { message: "Foo" }, 
      { message: "Bar" }
    ]
  }
});

v-for sur objet

<ul id="app">
  <li v-for="(value, key) in book">
    {{key}} : {{ value }}
  </li>
</ul>
var app = new Vue({
  el: "#app",
  data: {
    book: {
      title: "Learn Vue",
      author: "Jane Doe",
      publishedAt: "2016-04-10"
    }
  }
});

v-for indexé sur objet

<ul id="app">
  <li v-for="(value, key, index) in book">
    #{{index}} {{key}} : {{ value }}
  </li>
</ul>
var app = new Vue({
  el: "#app",
  data: {
    book: {
      title: "Learn Vue",
      author: "Jane Doe",
      publishedAt: "2016-04-10"
    }
  }
});

⚠️ L'ordonnancement des propriétés d'un objet JavaScript n'est pas garantie

v-for et key

<div id="app">
  <input
         :placeholder="'#' + i + ' - ' + item"
         v-for="(item, i) in items"
         :key="i"
   />
  <button @click="remove">Remove first</button>
</div>
var app = new Vue({
  el: "#app",
  data: {
    items: [4, 5, 9]
  },
  methods: {
    remove() {
      this.items.shift();
    }
  }
});

Définir key lors de l'usage de v-for est recommandé par Vue

v-for et key

<div id="app">
  <input
         :placeholder="'#' + i + ' - ' + item"
         v-for="(item, i) in items"
         :key="item"
   />
  <button @click="remove">Remove first</button>
</div>
var app = new Vue({
  el: "#app",
  data: {
    items: [4, 5, 9]
  },
  methods: {
    remove() {
      this.items.shift();
    }
  }
});

Mutations d'un tableau

<div id="app" @click="sort">
  {{ letters }}
</div>
var app = new Vue({
  el: "#app",
  data: {
    letters: ["J", "P", "B", "G"]
  },
  methods: {
    sort() {
      this.letters.sort();
    }
  }
});

Méthodes de mutations réactives

push(), pop(), shift(), unshift(),

splice(), sort(), reverse()

Filtrage d'un tableau

<ul id="app">
  <li v-for="n in even">{{ n }}</li>
</ul>
var app = new Vue({
  el: "#app",
  data: {
    numbers: [1, 2, 3, 4, 5]
  },
  computed: {
    even() {
      return this.numbers.filter(
        (n) => n % 2 === 0
      );
    }
  }
});

via une propriété calculée

Filtrage d'un tableau

<ul id="app">
  <li v-for="n in even()">
    {{ n }}
  </li>
</ul>
var app = new Vue({
  el: "#app",
  data: {
    numbers: [1, 2, 3, 4, 5]
  },
  methods: {
    even() {
      return this.numbers.filter(
        (n) => n % 2 === 0
      );
    }
  }
});

via une méthode

v-for et composants

<my-component v-for="item in items" :key="item.id"></my-component>
<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>

La directive v-for peut recevoir

 

La directive v-for peut fournir un index via

 

Vue peut suivre les mutations des méthodes auto-mutatives comme Array.push()

 

En cas de traitement lourd pour filtrer des données à passer dans un v-for, il vaut mieux utiliser

 

Gestion des évènements

Écouter des évènements

<div id="app">
  <button @click="counter += 1">
    Incrémenter
  </button>
  <p>Compteur : {{ counter }}</p>
</div>
var app = new Vue({
  el: "#app",
  data: {
    counter: 0
  }
});

Méthode de gestion d'évènement

<div id="app">
  <button @click="increment">
    Incrémenter
  </button>
  <p>Compteur : {{ counter }}</p>
</div>
var app = new Vue({
  el: "#app",
  data: {
    counter: 0
  },
  methods: {
    increment() {
      this.counter += 2;
    }
  }
});

Méthode avec paramètres

<div id="app">
  <button @click="increment(3)">
    Incrémenter
  </button>
  <p>Compteur : {{ counter }}</p>
</div>
var app = new Vue({
  el: "#app",
  data: {
    counter: 0
  },
  methods: {
    increment(delta) {
      this.counter += delta;
    }
  }
});

Modificateurs d'évènement

<div id="app">
  <p @click="log('p#1@click')">
    <input type="checkbox" @click="log('input#1@click')" /> @click
  </p>
  <p @click="log('p#2@click')">
    <input type="checkbox" @click.stop="log('input#2@click.stop')" />
    @click.stop
  </p>
  <p>
    <input type="checkbox" @click.prevent="log('input#3@click.prevent')" />
    @click.prevent
  </p>
  <p>
    <input type="checkbox" @click.once="log('input#3@click.once')" />
    @click.once
  </p>
  <p>
    <input
           type="checkbox"
           @click.prevent.once="log('input#4@click.prevent.once')"
           />
    @click.prevent.once
  </p>
  <p>Events : {{ events }}</p>
  <button @click="events = []">Clear</button>
</div>
var app = new Vue({
  el: "#app",
  data: {
    events: []
  },
  methods: {
    log(e) {
      this.events.push(e);
    }
  }
});

Modifient le traitement de l'évènement

Modificateurs d'évènement

Modificateurs d'évènement

Générique Clavier Système Souris
.stop
.prevent
.capture
.self
.once
.passive
.enter
.tab
.delete
.esc
.space
.up
.down
.left
.right
.ctrl
.alt
.shift
.meta
.exact
.left
.right
.middle
 

Exercice

Créer une todo list dans laquelle on puisse marquer les éléments comme supprimés.

 

Permettre trois modes de visualisation : "Tout", "Actifs", "Supprimés"

 

Exercice

Rajouter un champ de recherche pour filtrer la liste des tâches.

Liaison sur les champs de formulaire

v-model

Créer des liaisons bi-directionnelles entre le modèle et la vue.

v-model et champ texte

<div id="app">
  <input v-model="message" />
  <p>{{ message }}</p>
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: ""
  }
});

La magie derrière v-model

  <input v-model="message" />
  <input 
         :value="message" 
         @input="message = $event.target.value"
  />

v-model et zone de texte

<div id="app">
  <p style="white-space: pre-line;">{{ message }}</p>
  <textarea v-model="message"></textarea>
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: ""
  }
});

v-model et checkbox

<div id="app">
  <input 
         type="checkbox" 
         id="checkbox" 
         v-model="checked" 
  />
  <label for="checkbox">{{ checked }}</label>
</div>
var app = new Vue({
  el: "#app",
  data: {
    checked: true
  }
});

v-model et radio

<div id="app">
  <input type="radio" id="one" value="Un" v-model="picked" />
  <label for="one">Un</label><br />
  
  <input type="radio" id="two" value="Deux" v-model="picked" />
  <label for="two">Deux</label><br />
  
  <span>Choisi : {{ picked }}</span>
</div>
var app = new Vue({
  el: "#app",
  data: {
    picked: null
  }
});

v-model et select

<div id="app">
  <select v-model="selected">
    <option disabled value="">Choisissez</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Sélectionné : {{ selected }}</span>
</div>
var app = new Vue({
  el: "#app",
  data: {
    selected: ""
  }
});

v-model et choix multiples

<div id="app">
  <select v-model="selected" multiple>
    <option disabled value="">Choisissez</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Sélectionné : {{ selected }}</span>
</div>
var app = new Vue({
  el: "#app",
  data: {
    selected: []
  }
});

v-model et select dynamique

<div id="app">
  <select v-model="selected">
    <option 
            v-for="option in options" 
            :value="option.value"
    >
      {{ option.text }}
    </option>
  </select>
  <span>Sélectionné : {{ selected }}</span>
</div>
var app = new Vue({
  el: "#app",
  data: {
    selected: "A",
    options: [
      { text: "Un", value: "A" },
      { text: "Deux", value: "B" },
      { text: "Trois", value: "C" }
    ]
  }
});

v-model et value

<!-- `picked` sera une chaine de caractères "a" quand le bouton radio sera sélectionné -->
<input type="radio" v-model="picked" value="a">


<!-- `toggle` est soit true soit false -->
<input type="checkbox" v-model="toggle">


<!-- `selected` sera une chaine de caractères "abc" quand la première option sera sélectionnée -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

v-model et modificateurs

<div id="app">
  <p>
    v-model.lazy
    <input v-model.lazy="lazy" />
    <code>{{ lazy }}</code>
  </p>
  <p>
    v-model.number
    <input v-model.number="number" type="number" />
    <code>{{ typeof number }}</code>
    <code>{{ number }}</code>
  </p>
  <p>
    v-model.trim
    <input v-model.trim="trim" />
    <code>{{ trim }}</code>
  </p>
</div>
var app = new Vue({
  el: "#app",
  data: {
    lazy: "",
    number: null,
    trim: ""
  }
});

v-model et modificateurs

v-model et composants

Il est possible d'implémenter un v-model personnalisé pour ses composants.

 

Cela passe par la déclaration d'une prop de modèle (par défaut "value") et d'un événement de modèle (par défaut @input).

 

Mais on voit tout cela un peu plus tard ;)

Un évènement peut être écouter grâce à

 

Un évènement au sens de Vue possède

 

Un modificateur d'évènement

 

 

v-model peut s'utiliser

 

Récap'

Vue : architecture

vue

<template>

modèle

data

logique

methods

@event

this.datum = ...

render

Vue : instance

var app = new Vue({
});

Vue : instance

var app = new Vue({
  el: "#app"
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "..."
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "...",
  data: { ... }
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "...",
  data: { ... },
  computed: { ... }
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "...",
  data: { ... },
  computed: { ... },
  methods: { ... }
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "...",
  data: { ... },
  computed: { ... },
  methods: { ... },
  watch: { ... }
});

Vue : instance

var app = new Vue({
  el: "#app",
  template: "...",
  data: { ... },
  computed: { ... },
  methods: { ... },
            
  beforeCreated() { ... },
  created() { ... },
  beforeMounted() { ... },
  mounted() { ... },
  updated() { ... },
  destroyed() { ... }
});

Vue : composants

Composants

Exemple de base

<div id="app">
  <button-counter></button-counter>
</div>
Vue.component("button-counter", {
  name: "ButtonCounter",
  data() {
    return {
      count: 0
    };
  },
  template: '<button v-on:click="count++">{{ count }} clicks</button>'
});

var app = new Vue({
  el: "#app"
});

Ré-utilisabilité d'un composant

<div id="app">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
Vue.component("button-counter", {
  name: "ButtonCounter",
  data() {
    return {
      count: 0
    };
  },
  template: '<button v-on:click="count++">{{ count }} clicks</button>'
});

var app = new Vue({
  el: "#app"
});

État local d'un composant

<div id="app">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>
Vue.component("button-counter", {
  name: "ButtonCounter",
  data() {
    return {
      count: 0
    };
  },
  template: '<button v-on:click="count++">{{ count }} clicks</button>'
});

var app = new Vue({
  el: "#app"
});

Organisation des composants

Enregistrement global

// kebab-case
Vue.component("button-counter", {
  ...
});

// PascalCase
Vue.component("ButtonCounter", {
  ...
});
<div id="app">
  <button-counter></button-counter>
  <ButtonCounter></ButtonCounter>
</div>

Enregistrement local

let ComponentA = { /* ... */ }
let ComponentB = { /* ... */ }

// Dans l'instance de Vue
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

// Dans un composant
let ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

Enregistrement local + modules

import ComponentA from './ComponentA'
import ComponentB from './ComponentB'

// Dans l'instance de Vue
new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})

// Dans un composant
var ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

Composants

Les props

Les props : syntaxe

<!-- kebab-case en HTML -->
<blog-post post-title="Hello !"></blog-post>

<BlogPost post-title="Hello !"></BlogPost>
Vue.component('blog-post', {
  // camelCase en JavaScript
  props: ['postTitle'],
  template: '<h3>{{ postTitle }}</h3>'
})

Descente de props

<blog-post title="Mon initiation avec Vue"></blog-post>
<blog-post title="Blogger avec Vue"></blog-post>
<blog-post title="Pourquoi Vue est tellement cool"></blog-post>
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

Descente de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
></blog-post>
Vue.component('blog-post', {
  props: ['title'],
  template: '<h3>{{ title }}</h3>'
})

new Vue({
  el: '#app',
  data: {
    posts: [
      { id: 1, title: 'Mon initiation avec Vue' },
      { id: 2, title: 'Blogger avec Vue' },
      { id: 3, title: 'Pourquoi Vue est tellement cool' }
    ]
  }
})

Descente de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
></blog-post>
// Template du composant

<h3>{{ title }}</h3>

Descente de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
  :content="post.content"
></blog-post>
// Template du composant

<h3>{{ title }}</h3>
<div v-html="content"></div>

Descente de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
  :content="post.content"
></blog-post>
// Template du composant

<div class="blog-post">
  <h3>{{ title }}</h3>
  <div v-html="content"></div>
</div>

// Un composant doit avoir
// un unique élément racine

!!!

Descente de plus de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
  :content="post.content"
  :publishedAt="post.publishedAt"
  :comments="post.comments"
></blog-post>

Descente de plus de props

<blog-post
  v-for="post in posts"
  :key="post.id"
  :post="post"
></blog-post>

Type de props

props: ['title', 'likes', 'isPublished', 'commentIds', 'author']

Type de props

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function, // déprécié
  contactsPromise: Promise 
  // tout autre constructeur
}
<blog-post
  title="Lorem ipsum"
  :likes="3"
  :isPublished="false"
  :commentIds="[1,2,3]"
  :author="{name: 'Alice'}"
  :callback="() => alert('Done')"
></blog-post>

Validation des props

props: {
    propA: Number,
      
    propB: [String, Number],
      
    propC: {
      type: String,
      required: true
    }
  }

Validation des props

props: {
    propD: {
      type: Number,
      default: 100
    },
      
    propE: {
      type: Object,
      default: () => ({ message: 'hello' })
    },
      
    propF: {
      type: Array,
      default: () => []
    }
  }

Validation des props

props: {
    propG: {
      validator: value => ['A', 'B', 'C'].includes(value)
    }
  }

Composants

Les événements

Remonter d'évènements

// Template racine

<blog-post
  v-for="post in posts"
  :key="post.id"
  :post="post"
></blog-post>
// Template de <blog-post>

<div class="blog-post">
  <h3>{{ post.title }}</h3>
  <div v-html="post.content"></div>
  <button>
    Like
  </button>
</div>
  
// Remonter le like ?

Remonter d'évènements

// Template racine

<blog-post
  v-for="post in posts"
  :key="post.id"
  :post="post"
  @like="receiveLike"
></blog-post>
// Template de <blog-post>

<div class="blog-post">
  <h3>{{ post.title }}</h3>
  <div v-html="post.content"></div>
  <button  @click="$emit('like')">
    Like
  </button>
</div>
methods: {
  receiveLike() {
    ...
  }
}

Remonter d'évènements

// Template racine

<blog-post
  v-for="post in posts"
  :key="post.id"
  :post="post"
  @note="receiveNote"
></blog-post>
// Template de <blog-post>

<div class="blog-post">
  <h3>{{ post.title }}</h3>
  <div v-html="post.content"></div>
  <button  @click="$emit('note', 5)">
    Noter bien
  </button>
  <button  @click="$emit('note', 0)">
    Noter pas bien
  </button>
</div>
methods: {
  receiveNote(payload) {
    payload; // 0 ou 5
    ...
  }
}

Remonter d'évènements

// Template racine

<blog-post
  v-for="post in posts"
  :key="post.id"
  :post="post"
  @note="receiveNote"
></blog-post>
// Template de <blog-post>

<div class="blog-post">
  <h3>{{ post.title }}</h3>
  <div v-html="post.content"></div>
  <button  @click="love">Love</button>
  <button  @click="hate">Hate</button>
</div>
methods: {
  receiveNote(note) {
    ...
  }
}
methods: {
  love() {
    this.$emit('note', 5);
  }
}

Composants

Props + Événements = ❤️

v-model et composant

<blog-post-writer v-model="post">
</blog-post-writer>

// Comment faire ?
var vm = new Vue({
  el: "#app",
  data: { 
    post: "Hello world !"
  }
});

v-model et composant

<blog-post-writer v-model="post">
</blog-post-writer>
var vm = new Vue({
  el: "#app",
  data: { 
    post: "Hello world !"
  }
});
<blog-post-writer 
  :value="post" 
  @input="post = $event">
</blog-post-writer>

=

v-model et composant

var vm = new Vue({
  el: "#app",
  data: { 
    post: "Hello world !"
  }
});
Vue.component('blog-post-writer', {
  props: ['value'],
  template: `
    <p>
      <input 
         :value="value" 
         @input="$emit('input', 
                 $event.target.value)">
    </p>`
})
<blog-post-writer v-model="post">
</blog-post-writer>

props synchronisées

var vm = new Vue({
  el: "#app",
  data: { 
    post: "Hello world !"
  }
});
<blog-post-writer :content.sync="post">
</blog-post-writer>
Vue.component('blog-post-writer', {
  props: ['content'],
  template: `
    <p>
      <input 
         :value="content" 
         @input="$emit('update:content', 
                 $event.target.value)">
    </p>`
})

Composants

Les slots

Les slots

<alert-box>
  Quelque chose s'est mal passé.
</alert-box>

Les slots

<alert-box>
  Quelque chose s'est mal passé.
</alert-box>
Vue.component('alert-box', {
  template: `
    <div class="alert-box">
      <strong>Erreur !</strong>
      <slot></slot>
    </div>
  `
})

Les slots nommés

<alert-box>
  Quelque chose s'est mal passé.
  <template #code>1337</template>
</alert-box>
Vue.component('alert-box', {
  template: `
    <div class="alert-box">
      <strong>Erreur !</strong>
      <slot></slot>
      <i v-if="$slots.code"> Code <slot name="code" /> </i>
    </div>
  `
})

Composants

Single File Component

Composant monofichier

La jungle

Les composants dynamiques

<component v-bind:is="customAlertComponent"></component>
Vue.component('success-alert', { ... });
Vue.component('error-alert', { ... });

var app = new Vue({
  el: "#app",
  data: {
    customAlertComponent: 'success-alert' 
  }
});

Pour aller plus loin

  • Plugins / Vue.use
  • Slots paramétriques
  • Mixins
  • Import asynchrone de composant
  • <keep-alive>
  • $on, $off, $once
  • Injections de dépendances

Quizz

Les composants s'organisent

 

Un composant peut être

 

Comment évoluent les informations

dans l'instance de Vue ?

 

Une prop peut recevoir

 

Quels noms de composants sont valides lors d'une déclaration via Vue.component ?

 

Quels noms de props sont valides dans la déclaration JavaScript d'un composant ?

Quels noms de props sont valides dans la déclaration d'un template ?

Un slot peut recevoir

Quels syntaxes pourraient me permettre d'effectuer un binding bi-directionnel de "datum" ?

Un composant monofichier

 

Exercice: Tableau de score

Réaliser un tableau de score dont chaque ligne doit être un composant <TeamCounter>

 

Cahier des charges de TeamCounter :

 

Template voir ci-contre

 

Props

score.sync : Le score actuel de l'équipe

city: La ville de l'équipe

color: La couleur de l'équipe

 

Slots

#default: Le nom de l'équipe

 

Events

@update:score

@win : Déclencher lorsque le score dépasse    

30, contient la ville de l'équipe en payload

Vuex

Qu'est-ce que Vuex ?

  • Gestionnaire d'état
  • Architecture Flux
  • Librairie officielle
  • Centralisé
  • Prédictible
  • Modulaire

Équivalence de structure

// Store Vuex

{
  state: { ... },
    
  getters: { ... },

    
  mutations: { ... },

  actions: { ... }
       
};
// Composant Vue

{
  data: { ... },
    
  computed: { ... },
    
  
             
  methods: { ... }  
    
              
};

Le State

// Store Vuex

{
  state: { prixHT: 10 },
    
  getters: { ... },

    
  mutations: { ... },

  actions: { ... }
       
};
// Composant Vue
import { mapState } from 'vuex';
{
  computed: { 
    ...mapState(["prixHT"])
  },
    
    
             
  methods: { ... }  
    
              
};
<template>
  <button> {{ prixHT }} € </button>
</template>

Les accesseurs

// Store Vuex

{
  state: { count: 10 },
    
  getters: { 
    prixTTC(state) {
       return state.prixHT * 1.2       
    }
  },

  ...
};
// Composant Vue
import { mapGetters, ~} from 'vuex'
{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
    
    
             
  methods: { ... }  
              
};
<template>
  <button> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Les mutations (sync)

// Store Vuex

{
  ...
  
  mutations: {
    increment(state) {
      state.prixHT += 5;
    }
  }

  ...
};
// Composant Vue
import {mapMutations} from 'vuex'
{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
    
             
  methods: { 
    ...mapMutations(["increment"]) 
  }           
};
<template>
  <button @click="increment"> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Les mutations (sync)

// Store Vuex

{
  ...
  
  mutations: {
    increment(state, payload) {
      state.prixHT += payload;
    }
  }

  ...
};
// Composant Vue
import {mapMutations} from "vuex"
{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
    
             
  methods: { 
    ...mapMutations(["increment"]) 
  }           
};
<template>
  <button @click="increment(7)"> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Les actions (async)

// Store Vuex

{
  mutations: {
    increment(state, payload) {
      state.prixHT += payload;
    }
  }

  actions: {
    increase(context) {
      context.commit('increment', 3);
    }
  }
};
// Composant Vue
import { mapActions, ~} from 'vuex'
{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
             
  methods: {
    ...mapMutations(["increment"]),
    ...mapActions(["increase"])  
  }           
};
<template>
  <button @click="increase"> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Les actions (async)

// Store Vuex

{

  actions: {
    increase(context, payload) {
      setTimeout(() => {
        context.commit('increment', payload);
      }, 3000)     
    }
  }
  
};
// Composant Vue

{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
             
  methods: {
    ...mapMutations(["increment"]),
    ...mapActions(["increase"])  
  }           
};
<template>
  <button @click="increase(4)"> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Les actions (async)

// Store Vuex

{

  actions: {
    increase(context, payload) {
      context.commit("mutation", payload);    
      context.dispatch("actions", payload);    
      context.state;    
      context.getters;    
    }
  }
  
};
// Composant Vue

{
  computed: { 
    ...mapState(["prixHT"]),
    ...mapGetters(["prixTTC"])
  },
             
  methods: {
    ...mapMutations(["increment"]),
    ...mapActions(["increase"])  
  }           
};
<template>
  <button @click="increase(4)"> {{ prixHT }} € HT </button> {{ prixTTC }} € TTC
</template>

Installation

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

En dehors d'un composant

import store from "store.js";

store.commit("mutation", payload);    
store.dispatch("actions", payload);    
store.state;    
store.getters;    
// store.js
import Vuex from 'vuex'

export default new Vuex.store({
  state: ...,
  getters: ...,
  mutations: ...,
  actions: ...
});

Actions et Promesses

myAction(context, payload) {
  return new Promise(...);
}
store.dispatch('myAction', "...")
   .then(result => { ... })
   .catch(error => { ... })

L'état local existe toujours

Excercice : TodoList + Vuex

Reprendre l'exercice de la TodoList pour la connecter à un store Vuex

 

Exercice

Le projet

Objectifs, notations, tout ça

Objectifs, notations, tout ça

Si vous êtes bloqués, manifester vous


Github Issue / iut@chazelle.pro


https://github.com/benjaminchazelle/teach-vue-chat-client-starter


https://codesandbox.io/s/happy-pike-9kggu?file=/ROADMAP.md

Merci de votre attention

Made with Slides.com