Vue.js
Rappel en speed
Etat local
<script>
export default {
data() {
return {
myDatum: 42
}
}
}
</script>
Computed
<script>
export default {
data() {
return {
myDatum: 42,
}
},
computed: {
twoTimes() {
return this.myDatum * 2; // 84
}
}
}
</script>
Template
<template>
<p>
SOme stuff here
</p>
</template>
v-if
<template>
<p v-if="seen">Je suis visible</p>
</template>
v-for
<template>
<ul>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ul>
</template>
Descente de props
<template>
<child-component :value="myDatum" />
</template>
Ecoute d'événement
<template>
<child-component @select="doSomething" />
</template>
Remontée d'événement
<script>
export default {
...,
methods: {
select(id) {
this.$emit("select", id);
}
},
...
}
</script>
v-model
<template>
<input v-model="message" />
</template>
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
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
Vue en bref
By Benji Chaz
Vue en bref
- 58