# Vue.js Beers

Après avoir suivi les tutoriels sur AngularJS ou encore Angular, vous avez peut-être envie de tester un nouveau framwork Javascript. C'est ce que je vous propose de découvrir dans ce tutoriel autour du développement avec le framework Vue.js.

# Présentation

# Les objectifs

Dans ce tutoriel, vous allez apprendre à construire une application web basée sur Vue.js et à consommer l'API que vous avez construite dans le tutoriel Express Beers.

# Organisation du tutoriel

Le tutoriel est divisé en 5 étapes qui sont les suivantes :

  1. Application de départ
  2. Ajout de librairies
  3. Mise à jour du projet Express Beers
  4. Création du composant BeersList
  5. Création du composant BeerDetail

# Step 01 - Application de départ

# Installation de la CLI de Vue.js

Vue.js fournit une CLI qui va nous être utile tout au long de ce tutoriel.

Tout d'abord, nous allons l'installé (je pars du principe de Node.js est déjà installé sur votre machine).

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Au moment où j'écris ce tutoriel la version que j'utilise est :

vue --version
@vue/cli 4.5.9

# Initialisation d'un nouveau projet

Maintenant que la CLI de Vue.js est installée, nous allons pouvoir créer notre nouveau projet.

vue create vue-beers
Vue CLI v4.5.9
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint) 
  Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features 

Le nouveau projet est alors créé.

🎉  Successfully created project vue-beers.
👉  Get started with the following commands:
 $ cd vue-beers
 $ yarn serve

Maintenant, il ne nous reste plus qu'à le lancer.

cd vue-beers
yarn serve
 DONE  Compiled successfully in 3903ms
  App running at:
  - Local:   http://localhost:8081/ 
  Note that the development build is not optimized.
  To create a production build, run yarn build.

Premier démarrage

# Step 02 - Ajout de librairies

Afin de faciliter les développements de l'interface, on va aussi utiliser un framwork CSS Bootstrap Vue qui va nous fournir la plupart des éléments que nous allors utiliser pour afficher tout ce dont nous avons besoins dans nos écrans.

Tout d'abord, on va installer les packages nécessaires

# With npm
npm install bootstrap-vue bootstrap
# With yarn
yarn add bootstrap-vue bootstrap

Ensuite on va enregistrer BootstrapVue dans notre application dans le fichier main.js

import Vue from 'vue'
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// Install BootstrapVue
Vue.use(BootstrapVue)
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin)

Ainsi que les fichiers CSS pour les styles dans le fichier main.js

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

On va aussi avoir besoin d'ajouter les packages Axios pour faire les appels à l'API prou récupérer les données qui seront ensuite afficher dans les écrans

# With npm
npm install --save axios
# With yarn
yarn add axios

# Step 03 - Mise à jour du projet Express Beers

Lors du développement de l'API Express Beers, on lisait un fichier JSON que l'on retournait en réponse à l'appel.

Dans ce projet, on trouve aussi les images associées aux bières listées dans le fichier JSON ainsi que leur références. Afin de pouvoir les afficher dans notre application Vue à partir de l'API, on va modifier celle-ci afin de les récupérer sur notre serveur.

Dans le fichier index.js, modifier le code pour avoir accès aux images

app.use('/beers/img', express.static('beers/img'));
app.use('/assets/img', express.static('img'));
app.use(express.static('public'));

Relancer le serveur

node index.js
Listening at http://:::3000

# Step 04 - Création du composant BeersList

Maintenant que notre API tourne et que nous avons ajouté les librairies dont nous allons nous servir pour afficher les informations dans notre application web, nous allons créer un premier composant qui va afficher la liste des bières.

Dans le dossier components, créer un nouveau fichier BeersList.vue.

<template>
  <div></div>
</template>
<script>
export default {
  name: 'BeersList',
}
</script>
<style>
</style>

Un composant Vue comporte 3 parties :

  • <template></template> qui va contenir le code HTML du composant
  • <script></script> qui va contenir la partie traitement du composant
  • <style></style> pour les styles CSS supplémentaires ou qui ne seront appliquer qu'au composant (et à ses descendants)

Dans ce composant, nous allons afficher :

  • un titre
  • un tableau qui sera rempli à l'aide de l'API
  • une pagination

# Construisons le template

Dans la partie template, on ajoute d'abord un titre

<template>
  <div class="beer-list">
    <h1>Liste des bières</h1>
  </div>
</template>

Attention : un template ne peut contenir qu'un et un seul élèment <div>.

Maintenant, on ajoute le tableau. Pour cela, on va utiliser le tableau de BootstrapVue <b-table>.

<template>
  <div class="beer-list">
    <h1>Liste des bières</h1>
    <b-table
      show-empty
      striped
      hover
      :items="beersList"
      :fields="fields"
      :current-page="currentPage"
      :per-page="perPage"
    >
    </b-table>
  </div>
</template>

Ce tableau affiche une liste vide quand on a trouvé aucun résultat.

Et enfin, on ajoute la pagination sur les éléments du tableau.

<template>
  <div class="beer-list">
    <h1>Liste des bières</h1>
    <b-table
      show-empty
      striped
      hover
      :items="beersList"
      :fields="fields"
      :current-page="currentPage"
      :per-page="perPage"
    >
    </b-table>
    <b-col sm="7" md="6" class="my-1">
      <b-pagination
        v-model="currentPage"
        :total-rows="totalRows"
        :per-page="perPage"
        align="fill"
        size="sm"
        class="my-0"
      >
      </b-pagination>
    </b-col>
  </div>
</template>

# La partie script

Dans cette partie, nous allons définir les données et les méthodes appelées.

Commençons par les données

<script>
export default {
  name: 'BeersList',
  data() {
    return {
      beersList: [], // liste des bières affichées dans le tableau
      fields: [ // définition des champs du tableau avec le nom des colonnes, leur clé et si on veut les trier
        { key: 'img', label: 'Image', sortable: false },
        { key: 'name', label: 'Nom', sortable: true },
        { key: 'description', sortable: false },
        { key: 'alcohol', label: '° Alcool', sortable: true },
        { key: 'actions', label: 'Actions' },
      ],
      totalRows: 0, // total des résultats remontés
      currentPage: 1, // numéro de la page courante
      perPage: 5, // nombre de lignes affichées par page
    };
  },
}
</script>

Continuons avec les méthodes

<script>
import axios from 'axios';
export default {
  name: 'BeersList',
  data() {
    return {
      // liste des bières affichées dans le tableau
      beersList: [],
      // définition des champs du tableau avec le nom des colonnes, leur clé et si on veut les trier
      fields: [
        { key: 'img', label: 'Image', sortable: false },
        { key: 'name', label: 'Nom', sortable: true },
        { key: 'description', sortable: false },
        { key: 'alcohol', label: '° Alcool', sortable: true },
        { key: 'actions', label: 'Actions' },
      ],
      // total des résultats remontés
      totalRows: 0,
      // numéro de la page courante
      currentPage: 1,
      // nombre de lignes affichées par page
      perPage: 5,
    };
  },
  // méthode appelée lorsque le composant est chargé dans l'application
  mounted() {
    this.getBeersList();
  },
  methods: {
    getBeersList() { 
      // URL pour appeler l'API pour récupérer la liste des bières
      const url = 'http://localhost:3000/beers';
      // Appel de l'API à l'aide d'Axios
      axios.get(url).then((response) => {
        // Initialisation de la liste de bières avec le retour de l'API
        this.beersList = response.data;
        this.totalRows = this.beersList.length;
      }).catch((error) => console.log('error', error));
    },
  },
}
</script>

# La partie CSS

Vous l'avez sans doute remarqué lorsque l'on a écrit la partie HTML, on a utilisé une classe qui ne vient pas de Bootstrap beer-list.

Nous allons maintenant la définir.

.beers-list {
  text-align: center;
}

# Testons notre composant

Pour pouvoir vérifier que notre composant fonctionne correctement, il ne nous reste plus qu'à modifier le fichier App.vue pour l'appeler

<template>
  <div id="app">
    <!--Intègre le composant beers-list pour l'afficher-->
    <beers-list></beers-list>
  </div>
</template>
<script>
// Importe le composant dans le composant App
import BeersList from './components/BeersList.vue'
export default {
  name: 'App',
  components: {
    BeersList,
  },
}
</script>

On obtient alors l'écran suivant

Premier résultat

# Affichons les images

Pour le moment, dans notre tableau, nous affichons le chemin des images et pas les images elle-mêmes. Nous allons donc modifier notre tableau pour afficher les images correspondantes aux chemins.

<template>
  <div class="beer-list">
    <h1>Liste des bières</h1>
    <b-table
      show-empty
      striped
      hover
      :items="beersList"
      :fields="fields"
      :current-page="currentPage"
      :per-page="perPage"
    >
      <!-- Affichage de l'image associée au chemin -->
      <template #cell(img)="data">
        <img :src="`http://localhost:3000/${data.value}`" width="100">
      </template>
    </b-table>
    <b-col sm="7" md="6" class="my-1">
      <b-pagination
        v-model="currentPage"
        :total-rows="totalRows"
        :per-page="perPage"
        align="fill"
        size="sm"
        class="my-0"
      >
      </b-pagination>
    </b-col>
  </div>
</template>

Liste des bières avec leurs images

# Step 05 - Création du composant BeerDetail

Maintenant que l'on a récupéré la liste des bières, on voudrait afficher le détail d'une bière lorsqu'on clique dessus. Pour cela, on va créer un nouveau composant qui va afficher les détails d'une bière et que l'on va nommer BeerDetail.vue.

<template>
</template>
<script>
export default {
  name: 'BeerDetail',
}
</script>
<style>
</style>

Ce nouveau composant est une modale, c'est-à-dire qu'il s'ouvre par-dessus le tableau.

<template>
  <b-modal id="beer-detail-modal" :title="title" size="lg">
  </b-modal>
</template>
<script>
import axios from 'axios';
export default {
  name: 'BeerDetail',
  // Id passé depuis l'écran BeersList
  props: {
    id: String,
  },
  data() {
    return {
      beer: null,
      title: null,
    };
  },
  methods: {
    // Récupération du détail d'une bière
    getBeerDetail() {
      if (this.id) {
        const url = 'http://localhost:3000/beer/' + this.id;
        axios.get(url).then((response) => {
          this.beer = response.data;
          // Mise à jour du titre de la modale
          this.title = `Détail de la bière ${this.beer.name}`;
        }).catch((error) => console.log('error', error));
      }
    },
  },
  // Permet de détecter les changements des variables
  watch: {
    id() {
      // Dès que l'id de la bière sélectionnée change, on charge le détail
      this.getBeerDetail();
    }
  }
}
</script>

Maintenant on intègre le composant dans la liste des bières

<template>
  <div class="beers-list">
    <h1>Liste des bières</h1>
    <b-table
      show-empty
      striped
      hover
      :items="beersList"
      :fields="fields"
      :current-page="currentPage"
      :per-page="perPage"
    >
      <template #cell(img)="data">
        <img :src="`http://localhost:3000/${data.value}`" width="100">
      </template>
      <template #cell(actions)="data">
        <b-button size="sm" @click="getBeerDetail(data.item)" class="mr-1">
          <b-icon icon="search"></b-icon>
        </b-button>
      </template>
    </b-table>
    <b-col sm="7" md="6" class="my-1">
      <b-pagination
        v-model="currentPage"
        :total-rows="totalRows"
        :per-page="perPage"
        align="fill"
        size="sm"
        class="my-0"
      ></b-pagination>
    </b-col>
    <beer-detail :id="beerId"></beer-detail>
  </div>
</template>
<script>
import axios from 'axios';
import BeerDetail from './BeerDetail.vue';
export default {
  name: 'BeersList',
  components: {
    BeerDetail,
  },
  data() {
    return {
      beersList: [],
      fields: [
        { key: 'img', label: 'Image', sortable: false },
        { key: 'name', label: 'Nom', sortable: true },
        { key: 'description', sortable: false },
        { key: 'alcohol', label: '° Alcool', sortable: true },
        { key: 'actions', label: 'Actions' },
      ],
      totalRows: 1,
      currentPage: 1,
      perPage: 4,
      beerId: null,
    };
  },
  mounted() {
    this.getBeersList();
  },
  methods: {
    getBeersList() {
      const url = 'http://localhost:3000/beers';
      axios.get(url).then((response) => {
        this.beersList = response.data;
        this.totalRows = this.beersList.length;
      }).catch((error) => console.log('error', error));
    },
    getBeerDetail(item) {
      this.beerId = item.id;
      this.$bvModal.show('beer-detail-modal');
    },
  },
}
</script>

Premère modale

Finalisons l'affichage du détail d'une bière

<template>
  <b-modal id="beer-detail-modal" :title="title" size="lg">
    <b-card v-if="beer !== null"
      :img-src="`http://localhost:3000/${beer.label}`"
      bg-variant="dark" text-variant="white"
      img-height="250" img-top>
      <b-row>
        <b-col>
          <img :src="`http://localhost:3000/${beer.img}`">
        </b-col>
        <b-col>
          <b-row>
            <b class="mr-1">Description : </b>{{beer.description}}
          </b-row>
          <b-row>
            <b class="mr-1">Brasseur : </b><span>{{beer.brewery}}</span>
          </b-row>
          <b-row>
            <b class="mr-1">Service : </b>{{beer.serving}}
          </b-row>
          <b-row>
            <b class="mr-1">Disponibilité : </b> {{beer.availability}}
          </b-row>
          <b-row>
            <b class="mr-1">Alcool : </b>{{beer.alcohol}} %
          </b-row>
          <b-row>
            <b class="mr-1">Type : </b>{{beer.style}}
          </b-row>
        </b-col>
      </b-row>
    </b-card>
  </b-modal>
</template>
<script>
import axios from 'axios';
export default {
  name: 'BeerDetail',
  props: {
    id: String,
  },
  data() {
    return {
      beer: null,
      title: null,
    };
  },
  methods: {
    getBeerDetail() {
      if (this.id) {
        const url = 'http://localhost:3000/beer/' + this.id;
        axios.get(url).then((response) => {
          this.beer = response.data;
          this.title = `Détail de la bière ${this.beer.name}`;
        }).catch((error) => console.log('error', error));
      }
    },
  },
  watch: {
    id() {
      this.getBeerDetail();
    }
  }
}
</script>

Modale finalisée

# Conclusion

Et voilà ! Vous avez développé votre première application Vue.js dans un temps relativement court.

Maintenant, à vous de la faire évoluer au gré de vos envies !