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 Creative Lab)
Déc 2013 - Version 0.6
Oct 2015 - Version 1.0
Sep 2016 - Version 2.0
Fév 2022 - 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
// 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>
// Plus usuellement
import { createApp } from "vue"
Architecture
Instance
<div id="app"></div>
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount("#app")
Paradigme
Modèle
Vue
Logique
Modèle, logique et vue
<script setup>
import { ref } from 'vue'
const message = ref("Hello world")
</script>
<template>
<h1>{{ message }}</h1>
</template>
<style scoped>
h1 {
text-decoration: underline;
}
</style>
Option API → Composition API
<script setup>
import { ref, computed, watch } from "vue"
defineProps(['text'])
const textSize = computed(() =>
text.value.length
)
const sizeChanged = ref(0)
watch(textSize, () => sizeChanged.value++)
function reset {
sizeChanged.value = 0
}
</script>
<template>
{{ text }} | {{ mutationCount }}
</template>
<script>
export default {
props: ['text'],
data: () => ({
sizeChanged: 0
}),
computed: {
textSize() {
return this.text.length
}
},
watch: {
textSize() {
this.sizeChanged++
}
},
methods: {
reset() {
this.sizeChanged = 0
}
}
}
</script>
=
Modèle réactif
<script setup>
import { ref } from 'vue'
const message = ref("Hello world")
</script>
<template>
<h1>{{ message }}</h1>
</template>
Attribut dynamique
<script setup>
import { ref } from 'vue'
const url = ref("http://...")
</script>
<template>
<img v-bind:src="url" />
</template>
Rendu conditionnel
<script setup>
import { ref } from "vue"
const seen = ref(true)
</script>
<template>
<p v-if="seen">Je suis visible</p>
<pre>
En console, entrer :
window.seen.value = false
</pre>
</template>
Boucles
<script setup>
const verbs = [
{ text: 'Veni' },
{ text: 'Vidi' },
{ text: 'Vici' }
]
</script>
<template>
<ol>
<li v-for="verb in verbs"
:key="verb.text"
>
{{ verb.text }}
</li>
</ol>
</template>
Événements utilisateurs
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<p>{{ count }}</p>
<button v-on:click="increment">
Incrémenter !
</button>
</template>
Binding bidirectionnel
<script setup>
import { ref } from "vue"
const message = ref("Hello world !")
</script>
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
Les composants
// LightSaber.vue
<script setup>
</script>
<template>
<span>Yoda</span>
<span class="handle">|||||</span>
<span class="light">======</span>
</template>
<style scoped>
.handle {
display: inline-block;
background: grey;
}
.light {
display: inline-block;
background: lightgreen;
color: transparent;
}
</style>
<script setup>
import LightSaber from "./LightSaber.vue"
</script>
<template>
<LightSaber />
</template>
Les props
// LightSaber.vue
<script setup>
const props = defineProps({
owner: String,
color: String
})
</script>
<template>
<div>
<span>{{ owner }}</span>
<span class="handle">|||||</span>
<span class="light"
:style="{background: color}">
======
</span>
</div>
</template>
<style scoped>...</style>
<script setup>
import LightSaber from "./LightSaber.vue"
</script>
<template>
<LightSaber owner="Mace windu" color="magenta" />
</template>
Les props et les boucles
<script setup>
import LightSaber from "./LightSaber.vue"
const jedis = [
{ name: 'Obi-Wan', color: 'lightblue' },
{ name: 'Darth Vader', color: 'red' },
{ name: 'Rey', color: 'orange' },
]
</script>
<template>
<LightSaber
v-for="jedi in jedis"
:owner="jedi.name"
:key="jedi.name"
:color="jedi.color"
/>
</template>
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'état local
La directive v-model
Un composant Vue peut contenir
Exercice : List
Créer une micro application permettant d'ajouter des éléments à une liste à l'aide d'un champ texte et d'un bouton.
Syntaxe d'un template
Cycle de vie de Vue
Cycle de vie de Vue
Cycle de vie de Vue
Cycle de vie de Vue
Hooks de cycle de vie
<script setup>
import { onBeforeMount, ... } from "vue"
onBeforeMount(() => {
// La réactivité est en place
// mais le composant n'est pas monté
// au DOM
})
onBeforeUpdate(() => {
// Le DOM va être mis à jour suite
// à une mutation dans l'état local
})
onBeforeUnmount(() => {
// Le composant va être démonté du DOM
})
</script>
onMounted(() => {
// Le composant vient d'être monté au DOM
})
onUpdated(() => {
// Le DOM vient d'être mis à jour suite
// à une mutation dans l'état local
})
onUnmounted(() => {
// Le composant vient d'être démonté
// du DOM
})
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>
Binding d'attribut
<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 :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-bind, v-on, 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>
Text
Donnés en tant que 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>
Vue est "monté"
http://interactly.glitch.me
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 Sémantique difficile -->
</div>
...
<div>
{{ message.split('').reverse().join('') }} <!-- #2 Performance dégradée -->
</div>
...
<div>
{{ message.split('').reverse().join('') }} <!-- #3 Répétition évitable -->
</div>
Propriété calculée
<script setup>
import { ref, computed } from "vue"
const message = ref("Hello world")
const reversed = computed(() =>
message.value
.split('')
.reverse()
.join('')
)
</script>
<template>
<p>Message original : {{ message }}</p>
<p>Message inversé : {{ reversed }}</p>
</template>
computed vs function
const now = computed(() =>
Date.now()
)
<script setup>
// ...
const reverse = () =>
message.value
.split('')
.reverse()
.join('')
</script>
<template>
Message inversé : {{ reverse() }}
</template>
Différence : computed met en cache son résultat
computed vs watch
import { ref, watch } from "vue"
const firstname = ref("John")
const lastname = ref("Doe")
const fullname = ref("John Doe")
watch([firstname, lastname], () =>
fullname.value = `${firstname.value} ${lastname.value}`
)
import { ref, computed } from "vue"
const firstname = ref("John")
const lastname = ref("Doe")
const fullname = computed(() =>
`${firstname.value} ${lastname.value}`
)
Mutateur calculé
fullname.value= 'John Doe';
// =>
firstname.value; // 'John'
lastname.value; // 'Doe'
const fullname = computed({
get: () =>
`${firstname.value} ${lastname.value}`,
set: (_fullname) => {
[fn, ln] = _fullname.split(" ")
firstname.value = fn
lastname.value = ln
}
})
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
<script setup>
let isActive = ref(true)
let hasError = ref(false)
</script>
<template>
<div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div>
</template>
<!-- Résultat -->
<div class="static active"></div>
v-bind:class + syntaxe objet
<script setup>
let isActive = ref(true)
let hasError = ref(false)
let classObject = computed(() => { active: isActive, 'text-danger': hasError })
</script>
<template>
<div class="static" :class="classObject"></div>
</template>
<!-- Résultat -->
<div class="static active"></div>
v-bind:class + syntaxe tableau
<script setup>
let dynamicClass = ref("foo")
</script>
<template>
<div :class="['staticClass', dynamicClass]"></div>
</template>
<!-- Résultat -->
<div class="staticClass foo"></div>
v-bind:class + syntaxe tableau
<div v-bind:class="[isActive ? 'active' : '', errorClass]"></div>
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
v-bind:class sur composant
// Parent.vue
<Child class="baz boo" />
<!-- Résultat -->
<p class="foo bar baz boo">Hello world</p>
// Child.vue
<p class="foo bar">Hello world</p>
v-bind:style + syntaxe objet
<script setup>
let activeColor = ref("red")
let fontSize = ref(30)
</script>
<template>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
</template>
<!-- Résultat -->
<div style="color: red; font-size: 30px;"></div>
v-bind:style + syntaxe tableau
<div :style="[baseStyles, overridingStyles]"></div>
v-bind:style + préfixes CSS
<div :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
Quizz
https://interactly.glitch.me/
Quelles classes seront appliquées ?
<div class="foo" :class="{ bar: displayBar, baz, qux: false }"></div>
let foo = ref(false)
let bar = ref(true)
let displayBar = ref(false)
let baz = ref(false)
let qux = ref(true)
Quels styles seront appliqués ?
<div :style="{ backgroundColor, color: activeColor }"></div>
let activeColor = ref("red")
let color = ref("blue")
let fontSize = ref(30)
let backgroundColor = ref("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
Rendu de liste
<script setup>
const items = [
{ message: "Foo" },
{ message: "Bar" }
]
</script>
<template>
<ul>
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
</template>
v-for sur tableau
<script setup>
const items = [
{ message: "Foo" },
{ message: "Bar" }
]
</script>
<template>
<ul>
<li v-for="(item, index) in items">
#{{index}} {{ item.message }}
</li>
</ul>
</template>
v-for indexé
<script setup>
const book = {
title: "Learn Vue",
author: "Jane Doe",
publishedAt: "2016-04-10"
}
</script>
<template>
<ul>
<li v-for="(value, key) in book">
#{{key}} {{ value }}
</li>
</ul>
</template>
v-for sur objet
⚠️ L'ordonnancement des propriétés d'un objet JavaScript n'est pas garantie
<script setup>
const book = {
title: "Learn Vue",
author: "Jane Doe",
publishedAt: "2016-04-10"
}
</script>
<template>
<ul>
<li v-for="(value, key, index) in book">
#{{index}} {{key}} : {{ value }}
</li>
</ul>
</template>
v-for indexé sur objet
⚠️ L'ordonnancement des propriétés d'un objet JavaScript n'est pas garantie
v-for et key
<script setup>
import { ref } from "vue"
const fields = ref(["A", "B", "C"])
const removeField = () =>
fields.value.shift()
</script>
<template>
<input
:placeholder="'#' + i + ' - ' + field"
v-for="(field, i) in fields"
:key="i"
/>
<button @click="removeField">
Remove first field
</button>
</template>
v-for et key
<script setup>
import { ref } from "vue"
const fields = ref(["A", "B", "C"])
const removeField = () =>
fields.value.shift()
</script>
<template>
<input
:placeholder="'#' + i + ' - ' + field"
v-for="(field, i) in fields"
:key="field"
/>
<button @click="removeField">
Remove first field
</button>
</template>
Mutation d'un tableau
<script setup>
import { ref } from "vue"
let letters = ref(
["J", "X", "O", "B", "U", "I"]
)
let sort = () => letters.value.sort()
</script>
<template>
<div @click="sort">
{{ letters }}
</div>
</template>
Méthodes de mutations réactives
push(), pop(), shift(), unshift(),
splice(), sort(), reverse()
Filtrage d'un tableau
<script setup>
import { ref, computed } from "vue"
const numbers = ref([1, 2, 3, 4, 5])
const even = computed(() =>
numbers.value.filter(n => n%2 === 0)
)
</script>
<template>
<li v-for="n in even">{{ n }}</li>
</template>
v-for et composants
<Child v-for="item in items" :key="item.id" />
<Child
v-for="(item, index) in items"
:item="item"
:index="index"
:key="item.id"
/>
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
<script setup>
import { ref } from "vue"
const count = ref(0)
</script>
<template>
<p>{{ count }}</p>
<button v-on:click="count += 1">
Incrémenter !
</button>
</template>
Gestionnaire d'évènement
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value += 2
}
</script>
<template>
<p>{{ count }}</p>
<button v-on:click="increment">
Incrémenter !
</button>
</template>
Gestionnaire d'évènement
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value += 2
}
</script>
<template>
<p>{{ count }}</p>
<button v-on:click="increment">
Incrémenter !
</button>
</template>
Gestionnaire paramétré
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment(delta) {
count.value += delta
}
</script>
<template>
<p>{{ count }}</p>
<button v-on:click="increment(3)">
Incrémenter !
</button>
</template>
Modificateurs d'évènement
<script setup>
import { ref } from "vue"
const events = ref([])
function log(e) {
events.value.push(e)
}
</script>
<template>
<p @click="log('p@click')">
<button @click="log('button@click')">@click</button>
<button @click.stop="log('button@click.stop')">@click.stop</button>
<button @click.once="log('button@click.once')">@click.once</button>
<button @click.prevent="log('button@click.prevent')">
@click.prevent
</button>
<button @click.prevent.once="log('button@click.prevent.once')">
@click.prevent.once
</button>
</p>
<ul v-for="event in events">
<li>{{ event }}</li>
</ul>
<button @click="events = []">
Clear
</button>
</template>
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
<script setup>
import { ref } from "vue"
const message = ref("Hello world !")
</script>
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
La magie derrière v-model
<input v-model="message" />
<input
:value="message"
@input="message = $event.target.value"
/>
v-model et zone de texte
<script setup>
import { ref } from "vue"
const message = ref("")
</script>
<template>
<textarea v-model="message">
</textarea>
<p style="white-space: pre-line;">
{{ message }}
</p>
</template>
v-model et checkbox
<script setup>
import { ref } from "vue"
const checked = ref(true)
</script>
<template>
<input
type="checkbox"
id="checkbox"
v-model="checked"
/>
<label for="checkbox">
{{ checked }}
</label>
</template>
v-model et radio
<script setup>
import { ref } from "vue"
const picked = ref(null)
</script>
<template>
<input type="radio" id="one"
value="Un" v-model="picked" />
<label for="one">Un</label>
<input type="radio" id="two"
value="Deux" v-model="picked" />
<label for="two">Deux</label>
<span>Choisi : {{ picked }}</span>
</template>
v-model et select
<script setup>
import { ref } from "vue"
const selected = ref("")
</script>
<template>
<select v-model="selected">
<option disabled value="">
Choisissez
</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Séléction : {{ selected }}</span>
</template>
v-model et select multiple
<script setup>
import { ref } from "vue"
const selected = ref([])
</script>
<template>
<select v-model="selected" multiple>
<option disabled value="">
Choisissez
</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>Séléction : {{ selected }}</span>
</template>
v-model et select dynamique
<script setup>
import { ref } from "vue"
const selected = ref("A")
const options = ref([
{ text: "Un", value: "A" },
{ text: "Deux", value: "B" },
{ text: "Trois", value: "C" }
])
</script>
<template>
<select v-model="selected">
<option v-for="option in options"
:value="option.value"
key="option.value">
{{ option.text }}
</option>
</select>
<div>Sélectionné : {{ selected }}</div>
</template>
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
<template>
<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>
</template>
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 écouté grâce à
http://interactly.glitch.me
Un évènement au sens de Vue possède
Un modificateur d'évènement
v-model peut s'utiliser
Composants
Architecture
Exemple de base
<script setup>
import CounterButton from "./components/CounterButton.vue"
</script>
<template>
<CounterButton />
</template>
<script setup>
import { ref } from "vue"
const count = ref(0)
</script>
<template>
<button @click="count++">
{{ count }} clicks
</button>
</template>
Ré-utilisabilité
<script setup>
import CounterButton from "./components/CounterButton.vue"
</script>
<template>
<CounterButton />
<CounterButton />
<CounterButton />
</template>
<script setup>
import { ref } from "vue"
const count = ref(0)
</script>
<template>
<button @click="count++">
{{ count }} clicks
</button>
</template>
Enregistrement global
import { createApp } from 'vue'
import CounterButton from "./CounterButton.vue"
const app = createApp({})
// // kebab-case
app.component("counter-button", CounterButton)
// PascalCase
app.component("CounterButton", CounterButton)
<template>
<counter-button />
<CounterButton />
</template>
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
<!-- toujours en kebab-case -->
<template>
<BlogPost post-title="Hello !" />
^^^^^^^^^^
<blog-post post-title="Hello !" />
^^^^^^^^^^
</template>
// BlogPost.vue
<script setup>
defineProps([
'postTitle'
])
</script>
<template>
<h3>{{ postTitle }}</h3>
</template>
Descente de props
<template>
<BlogPost title="What is love ?" />
<BlogPost title="Baby don't hurt me" />
<BlogPost title="No more" />
</template>
// BlogPost.vue
<script setup>
defineProps([
'title'
])
</script>
<template>
<h3>{{ title }}</h3>
</template>
Descente de props
<script setup>
import { ref } from "vue"
const posts = ref([
{ id: 1, title: `What is love ?` },
{ id: 2, title: `Baby don't hurt me` },
{ id: 3, title: `No more` }
])
</script>
<template>
<BlogPost v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</template>
// BlogPost.vue
<script setup>
defineProps([
'title'
])
</script>
<template>
<h3>{{ title }}</h3>
</template>
Descente de props
<script setup>
import { ref } from "vue"
const posts = ref([
{
id: 1,
title: `What is love ?`,
content: `Lorem ipsum [...]`
},
...
])
</script>
<template>
<BlogPost v-for="post in posts"
:key="post.id"
:title="post.title"
:content="post.content"
/>
</template>
// BlogPost.vue
<script setup>
defineProps([
'title',
'content'
])
</script>
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
</template>
Descente de props
<script setup>
import { ref } from "vue"
const posts = ref([
...
])
</script>
<template>
<BlogPost v-for="post in posts"
:key="post.id"
:title="post.title"
:content="post.content"
:comments="post.comments"
:author="post.author"
:isSharable="post.isSharable"
:publishedAt="post.publishedAt"
/>
</template>
Descente de props
<script setup>
import { ref } from "vue"
const posts = ref([
...
])
</script>
<template>
<BlogPost v-for="post in posts"
:key="post.id"
:post="post"
/>
</template>
Typage de props
defineProps([
'title', 'likes', 'isPublished', 'commentIds', 'author'
])
Typage de props
defineProps({
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function, // déprécié
contactsPromise: Promise
// tout autre constructeur
})
<BlogPost
title="Lorem ipsum"
:likes="3"
:isPublished="false"
:commentIds="[1,2,3]"
:author="{name: 'Alice'}"
:callback="() => alert('Done')"
/>
Validation des props
defineProps({
propA: Number,
propB: [String, Number],
propC: {
type: String,
required: true
}
})
// Seulement éxécuté en dévelopment
Validation des props
defineProps({
propD: {
type: Number,
default: 100
},
propE: {
type: Object,
default: () => ({ message: 'hello' }) // Factory
},
propF: {
type: Array,
default: () => [] // Factory
}
})
Validation des props
defineProps({
propG: {
validator: value => ['A', 'B', 'C'].includes(value)
}
})
Composants
Les événements
// BlogPost.vue
<script setup>
defineProps(['title', 'content'])
</script>
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
<button>
Like
</button>
</template>
// Remonter le like ?
<script setup>
import { ref } from "vue"
const post = ref({...})
</script>
<template>
<BlogPost
:title="post.title"
:content="post.content"
/>
</template>
Remonter d'évènements
// BlogPost.vue
<script setup>
defineProps(['title', 'content'])
</script>
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
<button @click="$emit('like')">
Like
</button>
</template>
<script setup>
import { ref } from "vue"
const post = ref({...})
function receiveLike() {
...
}
</script>
<template>
<BlogPost
:title="post.title"
:content="post.content"
@like="receiveLike"
/>
</template>
Remonter d'évènements
// BlogPost.vue
<script setup>
defineProps(['title', 'content'])
</script>
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
<button @click="$emit('note', 5)">
Noter bien
</button>
<button @click="$emit('note', 0)">
Noter pas bien
</button>
</template>
<script setup>
import { ref } from "vue"
const post = ref({...})
function receiveNote(payload) {
payload; // 0 ou 5
}
</script>
<template>
<BlogPost
:title="post.title"
:content="post.content"
@note="receiveNote"
/>
</template>
Remonter d'évènements
// BlogPost.vue
<script setup>
defineProps(['title', 'content'])
const emit = defineEmits(['note'])
function love() {
emit('note', 999)
}
</script>
<template>
<h3>{{ title }}</h3>
<div v-html="content"></div>
<button @click="love">Love</button>
</template>
<script setup>
import { ref } from "vue"
const post = ref({...})
function receiveNote(payload) {
payload; // 999
}
</script>
<template>
<BlogPost
:title="post.title"
:content="post.content"
@note="receiveNote"
/>
</template>
Remonter d'évènements
Composants
Props + Événements = ❤️
v-model et composant
<script setup>
import BlogPostWriter from "./BPW.vue"
import { ref } from "vue"
const post = ref("Hello world !")
</script>
<template>
<div>{{ post }}</div>
<BlogPostWriter v-model="post" />
</template>
<script setup>
// Implémenter v-model
</script>
<template>
<div style="border: 1px solid red">
<input />
</div>
</template>
<script setup>
import BlogPostWriter from "./BPW.vue"
import { ref } from "vue"
const post = ref("Hello world !")
</script>
<template>
<div>{{ post }}</div>
<BlogPostWriter v-model="post" />
</template>
<script setup>
// Implémenter v-model
</script>
<template>
<div style="border: 1px solid red">
<input />
</div>
</template>
v-model et composant
<BlogPostWriter
:modelValue="post"
@update:modelValue="post = $event"
/>
<script setup>
import BlogPostWriter from "./BPW.vue"
import { ref } from "vue"
const post = ref("Hello world !")
</script>
<template>
<div>{{ post }}</div>
<BlogPostWriter v-model="post" />
</template>
<BlogPostWriter
:modelValue="post"
@update:modelValue="post = $event"
/>
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const update = $event => emit(
'update:modelValue',
$event.target.value
)
</script>
<template>
<div style="border: 1px solid red">
<input
:value="props.modelValue"
@input="update"
/>
</div>
</template>
v-model et composant
<script setup>
import BlogPostWriter from "./BPW.vue"
import { ref } from "vue"
const post = ref("Hello world !")
</script>
<template>
<div>{{ post }}</div>
<BlogPostWriter v-model:text="post" />
// D'autres v-model peuvent ainsi
// être passés
</template>
<script setup>
defineProps(['text'])
const emit = defineEmits(['update:text'])
const update = $event => emit(
'update:text',
$event.target.value
)
</script>
<template>
<div style="border: 1px solid red">
<input
:value="props.text"
@input="update"
/>
</div>
</template>
v-model et composant
Composants
Les slots
Les slots
<script setup>
import AlertBox from "./AlertBox.vue"
</script>
<template>
<AlertBox>
Quelque chose s'est mal passé.
</AlertBox>
</template>
// AlertBox.vue
<template>
<div style="background:red;">
<strong>Erreur</strong>
<slot />
</div>
</template>
Les slots nommés
<script setup>
import AlertBox from "./AlertBox.vue"
</script>
<template>
<AlertBox>
Quelque chose s'est mal passé.
<template #code>
1337
</template>
</AlertBox>
</template>
// AlertBox.vue
<template>
<div style="background:red;">
<strong>Erreur</strong>
<slot />
<i v-if="$slots.code">
Code <slot name="code" />
</i>
</div>
</template>
Pour finir
Option API
<script setup>
import { ref, computed, watch } from "vue"
defineProps(['text'])
const textSize = computed(() =>
text.value.length
)
const sizeChanged = ref(0)
watch(textSize, () => sizeChanged.value++)
function reset {
sizeChanged.value = 0
}
</script>
<template>
{{ text }} | {{ mutationCount }}
</template>
<script>
export default {
props: ['text'],
data: () => ({
sizeChanged: 0
}),
computed: {
textSize() {
return this.text.length
}
},
watch: {
textSize() {
this.sizeChanged++
}
},
methods: {
reset() {
this.sizeChanged = 0
}
}
}
</script>
=
Option API avec setup()
<script setup>
import { ref, computed, watch } from "vue"
defineProps(['text'])
const textSize = computed(() =>
text.value.length
)
const sizeChanged = ref(0)
watch(textSize, () => sizeChanged.value++)
function reset {
sizeChanged.value = 0
}
</script>
<template>
{{ text }} | {{ mutationCount }}
</template>
<script>
import { ref, computed, watch } from "vue"
export default {
props: ['text'],
setup() {
let textSize = computed(() =>
text.value.length
)
let sizeChanged = ref(0)
watch(textSize, () => sizeChanged.value++)
function reset {
sizeChanged.value = 0
}
return { text, sizeChanged }
}
}
</script>
=
Les composants dynamiques
<component :is="currentComponent"/>
<script setup>
import { ref } from "vue"
import ComponentA from "./ComponentA.vue"
import ComponentB from "./ComponentB.vue"
const currentComponent = ref(ComponentA)
function change() {
currentComponent.value = ComponentB
}
</script>
Reactivity API : reactive
<script setup>
import { ref, reactive } from "vue"
const nativeObj = { count: 0 }
const reactObj = reactive({
count: 0
})
</script>
<template>
<button @click="nativeObj.count++">
native = {{ nativeObj.count }}
</button>
<button @click="reactObj.count++">
react = {{ reactObj.count }}
</button>
</template>
Reactivity API : reactive
<script setup>
import { ref, reactive } from "vue"
const count = ref(1)
const obj = reactive({ count })
// Unwrapped : .value non requis
obj.count.value // ❌
obj.count === count.value // true
// Synchronisé
count.value++
count.value // 2
obj.count // 2
obj.count++
obj.count // 3
count.value // 3
</script>
<script setup>
import { ref, reactive } from "vue"
const array = reactive([
ref("Hello world")
])
// Wrapped : .value requis
array[0].value // "Hello world"
const map = reactive(new Map([
['key', ref("Lorem ipsum")]
]))
// Wrapped : .value requis
map.get('key').value // "Lorem ipsum"
</script>
Reactivity API : readonly
<script setup>
import { ref, readonly } from "vue"
const foo = readonly(ref(true))
foo.value = false
foo.value // true
const bar = readonly(9) // non réactif
bar // 9
</script>
<script setup>
import { reactive, readonly }
from "vue"
const foo = readonly(reactive({
tic: "tac"
}))
foo.tic = "toc"
foo.tic // "tac"
const bar = readonly({
tic: "tac"
}) // réactif
foo.tic = "toc"
bar.tic // "tac"
</script>
Pour aller plus loin
- Plugins / Vue.use
- Slots paramétriques
- Import asynchrone de composant
- <keep-alive>
- <teleport>
- <suspense>
- 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 app.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" ?
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
v-model:score : 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
Pinia
Qu'est-ce que Pinia ?
- Gestionnaire d'état
- Librairie recommandée
- Centralisation
- Hors cycle de vie des composants
- Modulaire
Modèle
Vue
Logique
Équivalence de structure
// userFooStore.js / Pinia
import { ref, computed } from "vue"
import { defineStore } from 'pinia'
export const useFooStore = defineStore('foo', () => {
const users = ref(...)
const userCount = computed(() => users.value.length)
function addUser(user) {
users.value.push(user)
}
return { users, userCount, addUser }
})
Utilisation du store
// Component.vue
<script setup>
import { ref, computed, toRefs } from "vue"
import { useFooStore } from "useFooStore.js"
const fooStore = useFooStore()
const { users, userCount } = toRefs(fooStore) // State & Getters
const { addUser } = fooStore // Actions
</script>
Bonne pratique : Encapsulation
State
Getters
Actions
(setters)
Privée
Publique
Installation
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
L'état local existe toujours
Exercice : Product Store
TypeScript
Typage statique
const message = "Hello!"
message()
// TS 2348 :
// This expression is not callable.
// Type 'String' has no call signatures
const luck = Math.random < 0.5
// TS 2365 :
// Operator '<' cannot be applied to
// types '() => number' and 'number'.()
function hello(person, date) {
return `Hello ${person}, today is ${date}!`
}
hello("Brendan")
// TS 7006 :
// Parameter 'date' implicitly has an 'any' type
Typage explicite
function hello(person: string, date: Date): string {
return `Hello ${person}, today is ${date.toLocaleString()}!`
}
hello("Brendan")
// TS 2345 :
// Argument of type 'string' is not assignable to parameter of type 'Date'
let foo = "dff"
foo = 42
// TS 2322 :
// Type 'number' is not assignable to type 'string'.
let bar: string | number = "dff"
bar = 42
// OK
Types
const a: string = ""
const b: number = 7
const c: boolean = true
const d: null = null
const e: number[] = [1, 2, 3]
const f: Array<number> = [1, 2, 3]
const g: Date = new Date()
const h: Set = new Set(["LEET", 1337])
const i: Set<string> = new Set(["LEET", "1337"])
const j: Record<string, number> = { "Zero": 0, "One": 1, "Two": 2 }
Type objet
function distance(A: { x: number, y: number }, B: { x: number, y: number }) {
return Math.sqrt((A.y - B.y)^2 + (A.x - B.x)^2);
}
type Point = { x: number, y: number }
function distance(A: Point, B: Point) {
return Math.sqrt((A.y - B.y)^2 + (A.x - B.x)^2);
}
TypeScript et Vue
<script setup lang="ts">
import { ref, computed } from "vue"
const foo = ref<number>(3)
foo.value = "Not a number" // Erreur
const bar = ref<string | null>(null)
const baz = computed<string>(() => bar.value) // Erreur
</script>
Vue.js devtools
Explorer le VDOM
Explorer les stores Pinia
Et enfin
Le moment que nous attendons tous
Très impatiamment
Le projet
Objectifs, notations, tout ça
Objectifs, notations, tout ça
Merci de votre attention
[9] Vue 3 + Pinia
By Benji Chaz
[9] Vue 3 + Pinia
- 213