Vue 3

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 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

Implémenter un magasin avec Pinia

 

Exercice

glitch.com/edit/#!/iut-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

Made with Slides.com