# Vue.js - Listes et composants

## Listes

Comme dans beaucoup d'application, nous aurons besoin d'afficher des listes de données.

### Associer un tableau à des éléments avec v-for

Nous pouvons utiliser la directive `v-for` pour faire le rendu d’une liste d’éléments en nous basant sur un tableau.&#x20;

La directive `v-for` utilise une syntaxe spécifique de la forme `item in items`, où `items` représente le tableau source des données et où `item` est un **alias** représentant l’élément du tableau en cours d’itération, l'élément actuellement parcourus.

```markup
<ul>
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>
```

```javascript
const items = ref([
    { message: 'Foo' },
    { message: 'Bar' }
])
```

Résultat :

* Foo
* Bar

### Exemple avec un tableau simple

* 5 :  `<li>` contant une boucle `v-for` qui va se répéter pour chaque éléments du tableau `listeDeCourses`.
  * Chaque élément du tableau `listeDeCourses` sera, à sont tour, injecté dans l'alias `article` puis affiché comme contenu du `<li>` : `{{ article }}`
* 13 : Déclaration du tableau `listeDeCourses`&#x20;

{% code lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <li v-for="article in listeDecourses">{{ article }}</li>
    </ul>
  </q-page>
</template>

<script setup>
import { ref } from 'vue'

const listeDecourses = ref(['Fromage', 'Pain', 'Vin blanc', 'Kirsch'])
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>
```

{% endcode %}

### �Exemple avec un tableau d'objets

* 5 - 8 : `<li>` contant une boucle `v-for` qui va se répéter pour chaque éléments du tableau `tasks` et affichage des différentes propriétés de l'objet retourné : `{{ task.name }}`
* 16 - 32 : Déclaration du tableau des tâches `tasks`
  * Chaque tâche est un objet avec un nom `name`, une date `dueDate` et un heure `dueTime` de fin.

{% code lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <li v-for="task in tasks">
        <div>{{ task.name }}</div>
        <small>{{ task.dueDate }} @{{ task.dueTime }}</small>
      </li>
    </ul>
  </q-page>
</template>

<script setup>
import { ref } from 'vue'

const tasks = ref([
  {
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  },
])
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>
```

{% endcode %}

### �Attribut unique :key

Afin d'identifier un élément d'une liste dans le but de le manipuler (supprimer, modifier, ... ), on utilise l’attribut `:key` dont **la valeur doit être unique dans une même liste.**

{% hint style="info" %}
**Il est conseillé de toujours préciser`:key` avec `v-for`.**

Ceci est utile autant pour l'optimisation que pour les transition avec le composant `<Transition />` de Vue.js
{% endhint %}

```markup
<div v-for="item in items" :key="item.id">
  {{ item.message }}
</div>
```

Si les éléments de la liste ne possèdent pas d'identifiant, il suffit d'utiliser leur index, leur position dans le tableau, comme présenté ci-après.

```markup
<ul>
  <li v-for="(item, index) in items" :key="index">
    {{ index }} - {{ item.message }}
  </li>
</ul>
```

### Exemple de liste avec bouton de suppression d'un élément

* 5 : Récupération de l'`index` dans `v-for` et création d'un attribut `:key` avec `index` comme valeur.
* 8 : Création d'un `<button>` qui au `click` appelle la méthode `deleteTask` en lui passant l'`index` en paramètre.
* 35 - 38 : Création de la méthode `deleteTask`

{% code lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">
        <div>{{ task.name }}</div>
        <small>{{ task.dueDate }} @{{ task.dueTime }}</small>
        <button @click="deleteTask(index)">X</button>
      </li>
    </ul>
  </q-page>
</template>

<script setup>
import { ref } from 'vue'

const tasks = ref([
  {
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  }
])

const deleteTask = (index) => {
  // Retire 1 élément à la position index du tableau tasks
  tasks.value.splice(index, 1)
}
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>

```

{% endcode %}

## �Composants

Une page se compose souvent de plusieurs parties: menu, entête, boutons, ... . Afin de faciliter la maintenance et la réutilisation de ces différents éléments, ils seront séparés dans plusieurs fichiers. Ce sont ses éléments que nous appelons des composants.

Les composants se trouvent dans le dossier `src/components` des projets Quasar.

### Créer un composant

Pour créer un composant, il suffit de créer un nouveau fichier `.vue` dans le dossier `src/components/`.

Si vous souhaitez savoir comment déclarer un composant un Vue.js de manière classique, voir la documentations officielle : <https://fr.vuejs.org/v2/guide/components-registration.html>.

### Exemple création d'un composant Task.vue

Ce composant contient toutes les informations d'une tâche.

Créer un fichier `Task.vue` dans le dossier `src/components/` de votre projet Qasar.

{% code title="components/Task.vue" %}

```markup
<template>
    <div>Je suis une tâche</div>
</template>
```

{% endcode %}

�Et voilà ! Vous venez de créer votre 1er composant :tada:&#x20;

### Utiliser un composant dans un autre composant, dans une page

En réalité, nos page sont également des composants Vue, vous aviez donc déjà travaillé avec des composants.

Pour pouvoir utiliser un composant dans un autre composant, il faut l'importer. Voici un exemple d'importation du composant `Task` dans un autre composant Vue.

```javascript
import task from 'components/Task.vue'
```

### �Exemple importation du composant Task.vue

Remplacez le code de votre fichier `pages/Index.vue` avec le code de l’exemple ci-apès.

* 4 : Utilisation du composant `Task` dans le template `<task></task>`
  * Les balises `<task></task>` seront remplacée par le contenu du `<template>` du composant `Task` soit : `<div>Je suis une tâche</div>`
* 11 - 13 : Importation du composant `Task` dans notre page `Index.vue`
  * Sans cette importation, impossible d'utiliser le composant `Task` dans la page

{% code title="pages/Index.vue" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <!-- Utilisation d'un composant Task -->
    <task></task>
  </q-page>
</template>

<script setup>
import task from 'components/Task.vue'
</script>
```

{% endcode %}

### �Passer des données à un composant via des propriétés

Pour l'instant notre composant Task est statique et afficher toujours la même chose. Nous allons maintenant y passer des données a afficher.

Pour que deux composants échanges des données, il doivent avoir une relation directe de type parent - enfant.

* **composant parent** : Le composant qui importe un autre composant
* **composant enfant** : le composant importé.

Pour passer des données à un composant, il faut que le composant enfant possède des propriétés.

Nous allons donc déclarer une propriété `task` et une propriété `index` à notre composant `Task.vue`.

* `task` : Cette propriété permettra de transmettre un objet contenant toutes les informations d'une tâche (nom, date et heure).
* `index` : la position de la tâche dans le tableau `tasks`

### Exemple création des propriétés task et index

Dans le composant `Task.vue` :

* 2 - 6 : Création du template avec les infos de la tâche et son index
  * Le template retourne un élément de liste `<li>` contant toutes les infos de la tâche.
  * `{{ task }}` permet d'accéder à une propriété d'un composant comme pour ses données et propriétés composées.
* 12 - 15 : Déclaration des propriétés du composant `Task.vue`
  * `task` : doit être de type `Object` et **est obligatoire**
  * `index` : doit être de type `Number` et **est obligatoire**

{% code title="components/Task.vue" lineNumbers="true" %}

```markup
<template>
  <li>
    <div>{{ props.task.name }}</div>
    <small>{{ props.task.dueDate }} @{{ props.task.dueTime }}</small>
    <button @click="deleteTask(props.index)">X</button>
  </li>
</template>

<script setup>
// Pas besoin d'importer la méthode, elle est transformée 
// par le transpilateur
const props = defineProps({
  task: Object,
  index: Number
})
</script>
```

{% endcode %}

�Dans la page `Index.vue` :

* 5 - 9 : Création d'une boucle `v-for` sur le composant `<task>` qui parcours le tableau `tasks` et injecte au composant la tâche et l'index en cours via les propriétés `:task` et `:index`.
  * Pour passer une donnée à une propriété d'un composant, on écrit le nom de la propriété précédé de `:`.
* 20 - 40 : Déclaration des données de la page.
  * Tableau des tâches `tasks`.
* 41 - 46 : Méthodes de la page.
  * `deleteTask(index)` supprime la tâche qui se situe à l'index passé en paramètre.

{% code title="pages/Index.vue" lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">
        <div>{{ task.name }}</div>
        <small>{{ task.dueDate }} @{{ task.dueTime }}</small>
        <button @click="deleteTask(index)">X</button>
      </li>
    </ul>
  </q-page>
</template>

<script setup>
import { ref } from 'vue'

const tasks = ref([
  {
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  }
])

const deleteTask = (index) => {
  // Retire 1 élément à la position index du tableau tasks
  tasks.value.splice(index, 1)
}
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>

```

{% endcode %}

### �Passer des données à un composant via un \<slot>

Une autre méthode pour passer des informations à un élément les \<slots>.

On déclare dans le composant un espace \<slot>\</slot> qui sera remplacé par le contenu saisi entre la balise d'ouverture et de fermeture d'un composant.

### Exemple composant avec slot

L'exemple suivant décrit un composant \<note> qui affiche le texte "Note :  " devant le contenu du composant.

* 4 : Ajout d'un slot au composant
  * Le contenu HTML saisi entre `<note>...</note>` sera injecté à la place de `<slot></slot>`

{% code title="components/Note.vue" %}

```markup
<template>
  <div>
    <strong>Note : </strong>
    <slot></slot>
  </div>
</template>
```

{% endcode %}

Dans une page on utilisera le composant `Note` ainsi :

* 5 - 6 : création de deux composants `<note>`
* 20 : importation du composant `Note.vue`

{% code title="pages/Index.vue" lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>

    <note>Prendre un sac</note>
    <note>Appeler le docteur Strange</note>

    <ul>
      <task v-for="(task, index) in tasks"
            :key="index"
            :task="task"
            :index="index"
      ></task>
    </ul>
  </q-page>
</template>

<script setup>
import task from 'components/Task.vue'
import note from 'components/Note.vue'
import { ref } from 'vue'

const tasks = ref([
  {
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  },
]);

const deleteTask = (index) => {
  tasks.value.splice(index, 1)
}
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>
```

{% endcode %}

### Les événements personnalisés pour communiquer aux parents

Le seul moyen pour un composant enfant de communiquer avec ses parents est de créer des **événements personnalisés qui seront écoutés par leur parents**.

Pour définir les évènements disponibles dans notre composant:

```javascript
const emits = defineEmits(['firstEvent', 'secondEvent'])
// Pour définir des paramètres
const emits = defineEmits({
    firstEvent: null, // Pas de paramètres
    secondEvent: (id) => true // Un paramètre 'id'
})
```

{% hint style="info" %}
`defineEmits` est une macro du compilateur; il n'y a pas besoin de l'importer
{% endhint %}

Pour déclencher un événement personnalisé dans le composant enfant :&#x20;

```javascript
emits('firstEvent')
```

Pour écouter un événement enfant depuis le parent

```markup
<my-component v-on:firstEvent="() => {...}"></my-component>
<my-component v-on:firstEvent="myMethod"></my-component>
<!-- @ est un raccourci pour v-on: -->
<my-component @firstEvent="() => {...}"></my-component>
```

### Exemple d'événement pour supprimer une tâche

Dans cet exemple afin de coller au mieux à la réalité, nous n'allons pas utiliser l'index du tableau pour supprimer une tâche, mais un identifiant unique `id` propre à la tâche, comme on le recevrais depuis une base de données.

Dans le composant `components/Task.vue`, nous allons supprimer la propriété `index` et dans le `<template>`  remplacer `deleteTask(index)` par `deleteTask(task.id)`.

* 5 : remplacement `deleteTask(index)` par `deleteTask(task.id)`.
* 13 : suppression de la propriété `index`

{% code title="components/Task.vue" lineNumbers="true" %}

```markup
<template>
  <li>
    <div>{{ task.name }}</div>
    <small>{{ task.dueDate }} @{{ task.dueTime }}</small>
    <button @click="() => deleteTask(props.task.id)">X</button>
  </li>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  task: Object
})
</script>
```

{% endcode %}

Dans `pages/Index.vue` on a ajouté un `id` à toutes les tâches, retiré l'`index` de la boucle `v-for`, passé `task.id` comme `:key` et modifié la méthode `deleteTask(id)` pour quelle supprime la tâche avec l'id correspondant et non la tâche à l'index correspondant.

* 5 : Suppression index de v-for
* 6 : Passage de l'id de la tâche comme identifiant unique `:key`
* 19, 25 et 31 : ajout d'un id à toutes les tâches
* 38 - 40 : modification de la méthode `deleteTask` pour qu'elle supprime la tâche avec l'id correspondant à son paramètre.&#x20;

{% code title="pages/Index.vue" lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <task v-for="task in tasks"
            :key="task.id"
            :task="task"
      ></task>
    </ul>
  </q-page>
</template>

<script setup>
import task from 'components/Task.vue'
import { ref } from 'vue'

const tasks = ref([
  {
    id: 123,
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    id: 432,
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    id: 496,
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  },
])

const deleteTask = (id) => {
  tasks.value = tasks.value.filter(task => task.id !== id)
}
</script>
```

{% endcode %}

{% hint style="info" %}
Ligne 39: `task => task.id !== id`

* Ceci est bien une fonction à flèche
* pas besoin de parenthèse si on n'a qu'un paramètre
* on peut faire une fonction flèchée sans accolades, ce qui aura pour effet de retourner directement la valeur indiquée.&#x20;

Ce code est donc équivalent à:

```javascript
(task) => {
    return task.id !== id
}
```

{% endhint %}

{% hint style="danger" %}
Problème, si on clique sur les boutons pour supprimer une tâche, on obtient le message suivant dans la console :

"Property or method "deleteTask" is not defined on the instance"
{% endhint %}

En effet notre composant `Task.vue` fait appel à une méthode `deleteTask` qui lui est inconnue. Un composant ne peut seulement faire appel à ses propres méthodes, il ne peut donc pas appeler celles de son parent.

C'est là qu'interviennent les événements personnalisés.&#x20;

1. Au click du bouton de suppression, nous allons émettre un événement `delete` qui contiendra l'id de la tache à supprimer.
2. Nous allons dire au parent d'écouter cet événement et d'appeler deleteTask lorsqu'il l'entendra.
3. deleteTask récupèrera l'id et supprimera la tâche.

Dans components/Task.vue

* 5 : au click, émission d'un événement `delete` contenant l'`id` de la tâche
* 14 - 16: définition de l'évènement

{% code title="components/Task.vue" lineNumbers="true" %}

```markup
<template>
  <li>
    <div>{{ task.name }}</div>
    <small>{{ task.dueDate }} @{{ task.dueTime }}</small>
    <button @click="() => emit('delete', task.id)">X</button>
  </li>
</template>

<script setup>
const props = defineProps({
  task: Object
})

const emit = defineEmits({
  delete: (id) => true
})
</script>

```

{% endcode %}

Dans la page Index.vue

* 8 : Ecoute de l'événement `delete` et appel de `deleteTask` lorsqu'il est entendu.

{% code title="pages/Index.vue" lineNumbers="true" %}

```markup
<template>
  <!-- q-page : composant page de Quasar avec marge intérieure -->
  <q-page padding>
    <ul>
      <task v-for="task in tasks"
            :key="task.id"
            :task="task"
            @delete="deleteTask"
      ></task>
    </ul>
  </q-page>
</template>

<script>
import task from 'components/Task.vue'
import { ref } from 'vue'

const tasks = ref([
  {
    id: 123,
    name: 'Prendre une douche',
    dueDate: '6.5.2020',
    dueTime: '12:00'
  },
  {
    id: 432,
    name: 'Coiffeur',
    dueDate: '15.5.2020',
    dueTime: '17:30'
  },
  {
    id: 496,
    name: 'Faire une lessive',
    dueDate: '10.5.2020',
    dueTime: '19:00'
  },
])

const deleteTask = (id) => {
  tasks.value = tasks.value.filter(task => task.id !== id)
}
</script>

<!-- Utilise le langage SCSS https://sass-lang.com/guide -->
<style lang="scss">

</style>
```

{% endcode %}

�Et voilà ! Vous savez maintenant créer des composants et comment ils peuvent communiquer entre eux.

&#x20;
