Page dynamique

Un fil récent sur le forum Laravel m’a paru suffisamment intéressant et didactique pour donner l’occasion d’un article. Le cas évoqué est assez fréquent et mérite qu’on s’y penche un peu. On a des données structurées dans un fichier JSON et on veut afficher une liste de nom et ensuite par un clic sur un nom afficher des détails.

On peut envisager plusieurs façons de réaliser cela. De façon traditionnelle on va passer par jQuery, commencer par envoyer la liste des noms et ensuite utiliser Ajax pour récupérer les informations sélectionnées. Évidemment on aura ainsi une requête pour chaque clic.

Une autre approche consiste à envoyer une page simple, celle-ci est équipée d’une routine Javascript pour aller chercher toutes les informations en bloc et les mémoriser. Ensuite chaque clic est traité en local de façon efficace.

Les données sont ici. Mais pour des raisons que je n’ai pas trop comprises on rapatrie ce fichier sur le serveur.

Le code du projet est disponible ici.

Version traditionnelle

J’ai déjà décrit cette version sur le forum mais ça sera plus lisible ici…

On va partir d’une installation fraîche de Laravel 5.7.

On place le fichier JSON en public/json/data.json. On change dans config/filesystems.php pour faciliter l’accès :

'disks' => [

    'local' => [
        'driver' => 'local',
        'root' => public_path(),
    ],

On prend ces deux routes (routes/web.php) :

Route::get('/', 'HomeController@index')->name('home');
Route::get('infos/{id}', 'HomeController@infos');

On crée ce code dans HomeController :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class HomeController extends Controller
{
    public function index()
    {
        $names = $this->getData()->pluck('name');

        return view('home', compact('names'));
    }

    public function infos($id)
    {
        $values = $this->getData()->forPage($id + 1, 1)->first();

        return view('values', compact('values'));
    }

    protected function getData()
    {
        $data = Storage::get('json/data.json');

        return collect(json_decode($data, true)['data']);
    }
}

On modifie ainsi la vue home :

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Recherche par nom</div>
                <div class="card-body">
                    <form>
                        <div class="form-group">
                            <select id="names" class="custom-select">
                                <option selected>Choisissez un nom</option>
                                @foreach($names as $name)
                                    <option value="{{ $loop->index }}">{{ $name }}</option>
                                @endforeach
                            </select>
                        </div>
                    </form>
                    <div id="infos"></div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

@section('scripts')

<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>

<script>
    $(function(){
        $('#names').change(function() {
            $.get('{{ url('infos') }}/'+ $(this).val(), function(data) {
                $('#infos').html(data);
            });
        });
    })
</script>

@endsection

Et on crée une nouvelle vue values :

<p>Nom : {{ $values['name']}}</p>
<p>Description : {!! $values['description'] !!}</p>
<p>Image : {{ $values['image']['full'] }}</p>

On se retrouve avec cette page en accueil :

Et quand on clique sur un nom de la liste :

Simple et efficace. Je ne détaille pas le code qui est classique. Juste une remarque concernant la simplicité du code permise par les collections de Laravel et l’approche déclarative bien plus lisible.

Version avec Vue.js

On va voir maintenant comment réaliser ça avec une approche SPA pilotée par Vue.js.

Il faut déjà installer les dépendances Javascript :

npm i

On crée une nouvelle route pour l’occasion :

Route::get('vueversion', 'HomeController@vue');

Dans HomeController on va juste envoyer une vue :

public function vue()
{
    return view('vue');
}

Et voici la vue :

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <league></league>
        </div>
    </div>
</div>
@endsection

Évidemment tout ça est très léger pour le moment. On voit qu’il est prévu un composant league de Vue. On va créer ce composant :

Avec ce code :

<template>
    <div class="card">
        <div class="card-header">Recherche par nom</div>
        <div class="card-body">
            <form>
                <div class="form-group">
                    <select v-model="selected" id="names" class="custom-select">
                        <option selected>Choisissez un nom</option>
                        <option v-for="(value, key) in items" :value="key" :key="key">{{ value.name }}</option>
                    </select>
                </div>
            </form>
            <div v-if="elements">
                <p>Nom : {{ elements.name}}</p>
                <p >Description : <span v-html=" elements.description"></span></p>
                <p>Image : {{ elements.image.full }}</p>
            </div>
        </div>
    </div>
</template>

<script>

export default {
    data () {
        return {
            selected: 'Choisissez un nom',
            items: []
        }
    },
    computed: {
        elements () {
            return this.items[this.selected];
        }
    },
    mounted () {
        window.axios.get('json/data.json').then(({ data }) => {
            let that = this;
            _.forEach(data.data, function(value, key) {
                that.items.push(value)
            });
        });
    }
}

</script>

On renseigne resources/js/app.js pour charger ce composant :

Vue.component('league', require('./components/League.vue'));

Il ne reste plus qu’à recompiler :

npm run dev (ou watch)

Maintenant à l’adresse …/vueversion on se retrouve avec la même page d’accueil :

Et le même fonctionnement qu’avec la version traditionnelle mais maintenant tout se joue en local et on n’a plus de requête au serveur pour chaque clic.

Voyons un peu plus en détail le composant créé…

On attend le chargement du DOM (mounted) pour lancer la requête Ajax pour récupérer toutes les données :

mounted() {
    window.axios.get('json/data.json').then(({ data }) => {
        let that = this;
        _.forEach(data.data, function(value, key) {
            that.items.push(value)
        });
    });
}

Dans l’installation de base de Laravel on dispose d’Axios. On récupère donc directement le fichier JSON et ensuite on remplit le tableau items.

Dans le template on remplit la liste de noms avec ce code :

<select v-model="selected" id="names" class="custom-select">
    <option selected>Choisissez un nom</option>
    <option v-for="(value, key) in items" :value="key" :key="key">{{ value.name }}</option>
</select>

La directive v-for parcourt la liste des items et on crée une option pour chaque nom. On crée une liaison avec la directive v-model. Ainsi la valeur de selected change à chaque changement de valeur sélectionnée dans la liste.

On a une propriété calculée pour l’affichage des éléments :

computed: {
    elements () {
        return this.items[this.selected];
    }
},

Donc quand selected change la valeur est recalculée et on a l’affichage dans le template :

<div v-if="elements">
    <p>Nom : {{ elements.name}}</p>
    <p >Description : <span v-html=" elements.description"></span></p>
    <p>Image : {{ elements.image.full }}</p>
</div>

On voit que la syntaxe de Vue.js est assez simple à mettre en œuvre.

Conclusion

Alors quelle version préférez-vous ?

Il est évident que la version SPA demande plus de travail et est peut-être moins intuitive mais côté efficacité il n’y a pas photo !




Comprendre Vue.js : Nuxt en action

Dans le précédent article je vous ai présenté Nuxt. On a vu qu’il simplifie le développement d’une application Vue en gérant par exemple de façon automatisé le routage. Dans le présent article je vous propose de l’utiliser pour une petite application de recherches d’informations nutritives à partir d’un code-barre. Ce genre d’application se multiplie étant donné la sensibilisation croissante aux méfaits des additifs et à l’équilibre alimentaire. Il existe une base de donnée ouverte renseignée par les utilisateurs qui expose une API que nous allons utiliser. C’est d’ailleurs cette API qui est exploitée par toutes les applications qui existent actuellement.

Open Food Facts

Open Food Facts est une initiative citoyenne mondiale indépendante de l’industrie à laquelle tout un chacun est invité à participer. Elle récence actuellement 452688 produits et la base s’agrandit tous les jours. Pour chaque produit on peut obtenir une foule d’informations comme les ingrédients utilisés, le nutriscore, le fabricant…

On crée le projet

On crée un projet Nuxt en mode SPA :

npx create-nuxt-app produits
npx: installed 402 in 10.099s
> Generating Nuxt.js project in E:\laragon\www\vue-tuto\produits
? Project name produits
? Project description Tout savoir sur les produits
? Use a custom server framework none
? Use a custom UI framework vuetify
? Choose rendering mode Single Page App
? Use axios module yes
? Use eslint yes
? Use prettier no
? Author name bestmomo
? Choose a package manager npm

J’ai ajouté Vuetify, Axios et Eslint. On se retrouve avec cette architecture :

On n’a plus qu’à lancer :

npm run dev

On attend que tout se construise…

Ça se termine par quelque chose comme ça :

Et on se retrouve avec la même page d’accueil que lors du précédent article :

Le layout et la page d’accueil

On va commencer par changer le layout (layouts/default.vue) :

<template>
  <v-app dark>
    <v-navigation-drawer
      v-model="drawer"
      clipped
      fixed
      app
    >
      <v-list>
        <v-list-tile
          to="/">
          <v-list-tile-action>
            <v-icon>find_in_page</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>Rechercher</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
        <v-list-tile
          href="https://fr.openfoodfacts.org"
          target="_blank">
          <v-list-tile-action>
            <v-icon>help</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>Comprendre</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar
      app
      fixed
      clipped-left>
      <v-toolbar-side-icon
        @click.stop="drawer = !drawer"/>
      <v-toolbar-title>Open Food facts</v-toolbar-title>
    </v-toolbar>
    <v-content>
      <v-container
        fluid
        fill-height>
        <nuxt />
      </v-container>
    </v-content>
  </v-app>
</template>

<script>
  export default {
    data: () => ({
      drawer: null
    })
  }
</script>

On change ainsi la mise en page globale :

Le lien Comprendre emmène sur le site d’open Food facts.

On peut supprimer la page inspire :

Et on met ce code dans index.vue :

<template>
  <v-layout
    column
    justify-center
    align-center>
    <v-flex
      xs12
      sm8
      md6>
      <div class="text-xs-center">
        <img src="https://static.openfoodfacts.org/images/misc/openfoodfacts-logo-fr-178x150.png">
      </div>
      <v-alert
        :value="alert"
        type="error">
        Aucun produit trouvé !
      </v-alert>
      <v-card>
        <v-card-title
          class="headline">
          Bienvenue sur Open Food Facts !
        </v-card-title>
        <v-card-text>
          <v-form @submit.prevent="submit">
            <v-text-field
              v-model="code"
              label="Entrez le code barre et appuyez sur 'Entrée'"
              type="number"
              required
            />
          </v-form>
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
export default {
  data() {
    return {
      code: '',
      alert: false
    }
  },
  methods: {
    submit() {
      // Ici il faut traiter la soumission
    }
  }
}
</script>

On va chercher le logo sur le site d’Open Food Facts. On crée un formulaire avec juste une zone de saisie pour le code barre et on prépare une alerte pour le cas où aucun produit n’est trouvé.

On peut supprimer les composants des logos devenus inutiles :

Appel de l’API et Vuex

On va utiliser Axios pour aller chercher les informations du produit qui correspond au code barre entré. Sur le site on trouve ces informations pour l’API :

Il est donc facile d’appeler cette API…

Maintenant où va-t-on placer les données reçues ? On veut les rendre disponibles pour plusieurs pages, donc plusieurs composants, dans ce cas le plus indiqué est de passer par Vuex. La bonne nouvelle c’est que Nuxt installe automatiquement Vuex ! On a un dossier store déjà présent :

On va créer un fichier index.js avec ce code :

import Vuex from 'vuex'

const createStore = () => {
  return new Vuex.Store({
    state: {
      data: {}
    },
    mutations: {
      setData (state, data) {
        state.data = data
      }
    }
  })
}

export default createStore

Juste un mutateur setData qui attend les informations pour les mettre dans data.

On va compléter la vue index.vue pour appeler l’API et remplir le store :

<script>
import axios from 'axios'

export default {
  data() {
    return {
      code: '',
      alert: false
    }
  },
  methods: {
    submit() {
      if(this.code != 0 && !isNaN(this.code)) {
        axios.get(`https://fr.openfoodfacts.org/api/v0/produit/${this.code}.json`)
        .then((res) => {
          if(res.data.status) {
            this.$store.commit('setData', res.data.product)
          } else {
            this.alert = true
          }
        })
      }
    }
  }
}
</script>

On utilise Axios et au retour on vérifie la donnée status qui nous indique si un produit a été trouvé. Si ce n’est pas le cas on change la valeur de et l’alerte devient visible :

Si le produit existe alors le store est rempli :

La vue des détails

Maintenant qu’on a récupéré les informations il faut les afficher, on va créer une vue details.vue :

Avec ce code :

<template>
  <v-layout>
    <v-flex
      xs12
      sm6
      offset-sm3>
      <v-card>
        <v-img :src="$store.state.data.image_url"/>

        <v-card-title primary-title>
          <div>
            <div class="headline">{{ $store.state.data.product_name }}</div>
            <span class="grey--text">{{ $store.state.data.generic_name }}</span>
          </div>
        </v-card-title>

        <v-card-text class="mb-4">
          <v-list>
            <v-list-tile>
              <v-list-tile-title><strong>Marque :</strong> {{ $store.state.data.brands }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile v-if="$store.state.data.serving_size">
              <v-list-tile-title><strong>Quantité :</strong> {{ $store.state.data.serving_size }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile>
              <v-list-tile-title><strong>Conditionnement :</strong> {{ $store.state.data.packaging_tags.join(', ') }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile>
              <v-list-tile-title><strong>Magasins :</strong> {{ $store.state.data.stores }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile v-if="$store.state.data.manufacturing_places">
              <v-list-tile-title><strong>Fabrication :</strong> {{ $store.state.data.manufacturing_places }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile v-if="$store.state.data.labels">
              <v-list-tile-title>{{ $store.state.data.labels }}</v-list-tile-title>
            </v-list-tile>
            <v-list-tile>
              <img
                :src="nutriscore($store.state.data.nutrition_grades)"
                width="120px"
                class="pt-4">
            </v-list-tile>
          </v-list>
        </v-card-text>

      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
export default {
  name: 'Details',
  methods: {
    nutriscore: function(value) {
      return `https://static.openfoodfacts.org/images/misc/nutriscore-${value}.svg`
    }
  }
}
</script>

On sait que Nuxt va automatiquement créer une route /details. Donc dans la vue index.vue on appelle cette route :

if(res.data.status) {
  this.$store.commit('setData', res.data.product)
  this.$router.push('/details')
} else {

Et maintenant on obtient les détails du produit :

Les ingrédients

On ne va pas s’arrêter en si bon chemin et on va ajouter une vue pour afficher les ingrédients :

Avec ce code :

<template>
  <v-layout>
    <v-flex
      xs12
      sm6
      offset-sm3>
      <v-card>
        <v-img
          :src="$store.state.data.image_ingredients_url"/>

        <v-card-title primary-title>
          <div>
            <div class="headline">{{ $store.state.data.product_name }}</div>
            <span class="grey--text">{{ $store.state.data.generic_name }}</span>
          </div>
        </v-card-title>

        <v-card-text>
          <p><strong>Liste  des ingrédients :</strong></p>
          <p>{{ $store.state.data.ingredients_text_fr }}</p>
        </v-card-text>

        <v-card-actions>
          <v-btn
            flat
            to="/details"
            color="orange">Détails</v-btn>
        </v-card-actions>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
export default {
  name: 'Components'
}
</script>

Et dans la vue details on ajoute un bouton pour accéder à cette nouvelle vue :

<v-card-actions>
  <v-btn
    flat
    to="/components"
    color="orange">Composants</v-btn>
</v-card-actions>

On a donc maintenant accès aux ingrédients des produits :

Conclusion

On voit ainsi qu’il est très facile de créer des vues avec des routes qui se créent automatiquement. D’autre part Vuex nous permet d’accéder aux données à partir de n’importe quel composant de l’application, ce qui est bien pratique.

 

 




Comprendre Vue.js : Nuxt

On a vu au cours des précédents articles que Vue se présente comme un outil simple et puissant. D’autre part il bénéficie d’une communauté très active et ses possibilités s’élargissent rapidement. Dans le présent article je vous présente Nuxt, un framework dont l’ambition n’est rien moins que créer des applications Vue.js universelles.

L’idée est d’utiliser le SSR (Server Side Rendering). C’est quoi cette bête ? Imaginez que vous créez une application avec pas mal de manipulation de code côté client, vous risquez deux écueils : votre application risque de perturber les moteurs de recherche, ça peut poser des soucis parfois au navigateur qui se retrouve avec un gros boulot. La solution est juste ment le SSR ! On crée l’application côté serveur et on envoie ce qu’il faut au client. En fait on a un isomorphisme entre le serveur et le client, le code est interprété de la même façon des deux côtés.

Évidemment côté serveur on a besoin de Node.js et cette approche ne fait pas l’unanimité… Mais Nuxt a d’autres tours dans son sac : on peut créer une SPA (Single Page Application) ou générer une application statique !

Sous le capot Nuxt utilise ce qu’on a déjà vu : Vue 2.0, Vue-Router, Vuex…

On va un peu s’amuser avec Nuxt pour comprendre son fonctionnement et voir ce qu’il apporte réellement…

Installation

On crée une application Nuxt à partir de Vue Cli :

npx create-nuxt-app test

...
? Project name test
? Project description Un essai de Nuxt
? Use a custom server framework none
? Use a custom UI framework vuetify
? Choose rendering mode Universal
? Use axios module yes
? Use eslint yes
? Use prettier no
? Author name bestmomo
? Choose a package manager npm

J’ai sélectionné : vuetify et axios.

D’autre part j’ai choisi le mode universel, c’est à dire avec la génération côté serveur, l’autre choix est SPA (Single Page Application) avec juste une génération côté client.

Là il faut attendre un certain temps que tout s’installe… Si tout va bien ça se termine avec ça :

On lance en mode développement :

On se rend donc à l’adresse localhost:3000 :

J’ai cet aspect parce que j’ai ajouté Vuetify dans l’installation. C’est un superbe framework pour le Material Design.

La structure de l’application

On a cette structure de dossiers :

Faisons un peu le point :

  • assets : les ressources non compilées comme les images, le CSS, SASS ou LESS
  • components : les composants Vue.js
  • layouts : les mises en page
  • middleware : si on veut une action avant différents événements comme par exemple le changement de page
  • pages : contient des composants Vue.js pour le routage, la structure de ce dossier va créer automatiquement les routes
  • plugins : si on veut ajouter des plugins à Vue.js
  • static : les fichiers statiques comme robots.txt
  • store : le dossier pour Vuex

Les routes

Comme je l’ai dit ci-dessus le routage avec Nuxt est automatiquement créé pour vue-router en suivant l’arborescence du dossier pages. On a actuellement ces deux composants :

Si vous aller voir dans le fichier nuxt/router.js vous allez trouver ce code :

export function createRouter() {
  return new Router({
    mode: 'history',
    base: '/',
    linkActiveClass: 'nuxt-link-active',
    linkExactActiveClass: 'nuxt-link-exact-active',
    scrollBehavior,

    routes: [{
      path: "/inspire",
      component: _2ffe4ba3,
      name: "inspire"
    }, {
      path: "/",
      component: _ba1b444a,
      name: "index"
    }],

    fallback: false
  })
}

On voit les deux routes créées dans ce composant de Nuxt.

On va voir si la création dynamique fonctionne, on crée un nouveau dossier avec un composant :

Avec ce code (la syntaxe des composants utilisés est liée à Vuetify) :

<template>
  <v-layout>
    <v-flex text-xs-center>
      Un essai du routeur
    </v-flex>
  </v-layout>
</template>

On voit que le routeur se met à jour :

routes: [{
  path: "/inspire",
  component: _2ffe4ba3,
  name: "inspire"
}, {
  path: "/articles/essai",
  component: _5bd398d9,
  name: "articles-essai"
}, {
  path: "/",
  component: _ba1b444a,
  name: "index"
}],

Et si on entre l’adresse on a bien la page créée :

C’est vraiment pratique !

On peut aussi très facilement créer une route dynamique avec un paramètres en prévoyant un souligné, par exemple :

Et dans le routeur on retrouve le paramètre :

}, {
  path: "/articles/:id?",
  component: _633358a6,
  name: "articles-id"
}, {

Maintenant avec ce code dans le composant :

<template>
  <v-layout>
    <v-flex text-xs-center>
      On a l'id {{ this.$route.params.id }}
    </v-flex>
  </v-layout>
</template>

Ça fonctionne :

Il est aussi possible de créer de jolie transitions, je vous laisse consulter la documentation.

De la même manière on peut accomplir une action avec le chargement des pages avec un middleware.

Mise en page

Voyons maintenant la création des pages. Voici l’organisation globale :

Le document

Le premier niveau est le document. par défaut on a un fichier .nuxt/views/app.template.html :

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

On peut intervenir à ce niveau pour une mise en forme qui concerne toute l’application en surchargeant ce fichier avec un fichier app.html à la racine mais c’est pas très utile.

Les layouts

Au second niveau on a les layouts (mises en page) qui permettent une mise en page personnalisée. On en a une par défaut à l’installation :

Comme j’ai ajouté Vuetify ce layout est assez fourni avec une navigation élégante. L’emplacement pour les pages se situe ici :

<v-content>
  <v-container>
    <nuxt />
  </v-container>
</v-content>

c’est le composant nuxt qui permet l’affichage de la page.

Les pages

Enfin les pages ont aussi la liberté d’avoir leur propre présentation. Ce sont évidemment des composants de Vue. Mais le plus important à savoir pour les pages c’est qu’elles disposent de clés spéciales qui rendent le développement plus facile :

  • asyncData : pour récupérer des données du serveur avec Axios si on utilise pas Vuex :
export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(`https://depot/articles/${id}`)
    return { titre: data.titre}
  }
}
  • fetch : pour remplir le store avant le chargement de la page (documentation ici).
  • head : pour définir des metas pour la page
  • layout : pour choisir un layout pour la page
  • transition : pour avoir un effet de transition pour la page (documentation ici)
  • middleware : pour définir un middleware pour la page

La liste n’est pas complète !

Un exemple

Les données

Arrivé à ce stade on se dit qu’un petit exemple ne ferait pas de mal… On va créer un fichier de données db.json à la racine avec ce code :

{
  "continents" : [
    { "id": 1, "name": "Europe" },
    { "id": 2, "name": "Amérique" },
    { "id": 3, "name": "Asie" }
  ],
  "countries" : [
    { "id": 1, "continentID": 1, "name": "France"},
    { "id": 2, "continentID": 1, "name": "Angleterre"},
    { "id": 3, "continentID": 1, "name": "Espagne"},
    { "id": 4, "continentID": 1, "name": "Italie"},
    { "id": 5, "continentID": 1, "name": "Etats-Unis"},
    { "id": 6, "continentID": 2, "name": "Vénézuéla"},
    { "id": 7, "continentID": 2, "name": "Canada"},
    { "id": 8, "continentID": 2, "name": "Colombie"},
    { "id": 9, "continentID": 2, "name": "Chili"},
    { "id": 10, "continentID": 3, "name": "Chine"},
    { "id": 11, "continentID": 3, "name": "Inde"},
    { "id": 12, "continentID": 4, "name": "Corée"},
    { "id": 13, "continentID": 5, "name": "Russie"}
  ]
}

Et on lance un serveur pour notre petite API (si vous n’avez pas json-server installé alors installez-le) :

json-server db.json --port 3001

J’ai changé le port par défaut qui est 3000 pour ne pas entrer en conflit avec Nuxt qui l’utilise aussi.

On peut maintenant accéder aux continents avec http://localhost:3010/continents et à un continent spécifique avec http://localhost:3010/continents/id. Et c’est la même chose pour les pays.

Les continents

On crée un fichier pages/continents.vue avec ce code :

<template>
  <v-layout>
    <v-flex
      xs12
      sm6
      offset-sm3>
      <v-card>
        <v-toolbar
          color="indigo"
          dark>
          <v-toolbar-title>Continents</v-toolbar-title>
        </v-toolbar>
        <v-list>
          <v-list-tile
            v-for="continent in continents"
            :key="continent.id"
          >
            <v-list-tile-content>
              <v-list-tile-title>{{ continent.name }}</v-list-tile-title>
            </v-list-tile-content>
          </v-list-tile>
        </v-list>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
import axios from 'axios'

export default {
  name: 'Continents',
  async asyncData () {
    return axios.get('http://localhost:3010/continents')
    .then((res) => {
      return {
        continents: res.data
      }
    })
  }
}
</script>

On utilise Axios avec une promesse, quand les données arrivent on renseigne la variable continents qui vient fusionner avec les données du composant (si elles existaient !). Ensuite dans le template on utilise v-for pour afficher tous les noms des continents avec l’url …/continents :

On voit qu’on s’en sort très simplement !

Maintenant j’aimerais qu’en cliquant sur un nom de continent j’ouvre la page des pays de ce continent. On a le choix entre utiliser un composant nuxt-link (méthode préconisée par la documentation), soit passer par une méthode. Je vais opter pour cette seconde solution pour le pas perturber le style de la page :

<template>

  ...

            <v-list-tile-content>
              <v-list-tile-title
                @click="countries(continent.id)"
              >{{ continent.name }}</v-list-tile-title>
            </v-list-tile-content>

  ...

</template>

<script>

  ...

  methods: {
    countries(id) {
      this.$router.push(`/countries/${id}`)
    }
  }
}
</script>

On récupère l’id du continent et on utilise la route countries avec ce paramètre.

Les pays

Maintenant on va créer le composant pour afficher les pays. Comme on a une route dynamique on adopte ce que je vous ai déjà décrit plus haut :

Avec ce code :

<template>
  <v-layout>
    <v-flex
      xs12
      sm6
      offset-sm3>
      <v-card>
        <v-toolbar
          color="indigo"
          dark>
          <v-toolbar-title>Pays</v-toolbar-title>
        </v-toolbar>
        <v-list>
          <v-list-tile
            v-for="country in countries"
            :key="country.id"
          >
            <v-list-tile-content>
              <v-list-tile-title>{{ country.name }}</v-list-tile-title>
            </v-list-tile-content>
          </v-list-tile>
        </v-list>
      </v-card>
    </v-flex>
  </v-layout>
</template>

<script>
import axios from 'axios'

export default {
  name: 'Countries',
  async asyncData ({ params }) {
    return axios.get(`http://localhost:3010/countries?continentID=${params.id}`)
    .then((res) => {
      return {
        countries: res.data
      }
    })
  }
}
</script>

On utilise encore Axios pour envoyer une requête, cette fois paramétrée. Ensuite on utilise v-for pour afficher les pays.

Toujours aussi simple !

Mais que se passe-t-il si on utilise un paramètre qui n’est pas un nombre ? On va se retrouver avec une page vide. Il est possible d’effectuer une validation avec la méthode validate :

export default {
  name: 'Countries',
  validate ({ params }) {
    // Doit être un nombre
    return /^\d+$/.test(params.id)
  },

Maintenant si le paramètre n’est pas un nombre on obtient la page d’erreur par défaut de Nuxt :

On peut créer sa page personnalisée en ajoutant un composant layouts/error.vue.

Les plugins

Il y a quand même un petit souci avec notre exemple : on importe deux fois Axios. Il serait bien de ne le faire qu’une fois… Nuxt permet cela. On a un fichier nuxt.config.js avec toute la configuration par défaut. On va juste déclarer axios dans ce fichier :

build: {
  vendor: ['axios'],
  
  ...

}

On peut ensuite l’importer dans plusieurs modules, il ne sera chargé qu’une fois !

C’est le même principe si on veut utiliser un autre plugin, on l’ajoute dans vendor.

Générer un projet

Il y a deux possibilités pour la génération :

  • next build : application avec un serveur web
  • next generate : application statique

On va utiliser cette seconde possibilité pour notre exemple.Mais on va quand même préparer le terrain en modifiant le composant index.vue :

<template>
  <v-layout
    column
    justify-center
    align-center>
    <v-flex
      xs12
      sm8
      md6>
      <v-card>
        <v-card-title class="headline">Un essai de Nuxt</v-card-title>
        <v-card-text>
          <nuxt-link
            class="white--text"
            to="/continents">
            Continents
          </nuxt-link>
        </v-card-text>
      </v-card>
    </v-flex>
  </v-layout>
</template>

On lance maintenant la génération et on se retrouve avec un dossier dist :

Et ça fonctionne !

Conclusion

Nuxt est un framework plutôt intéressant et bien conçu, il mérite d’être utilisé ! Même si vous n’êtes pas partant pour du SSR parce que Node n’est pas votre tasse de thé vous pouvez très bien l’utiliser en mode SPA (donc juste côté client) ou en génération statique comme on l’a vu ci-dessus.

Et Laravel ? Oui après tout c’est un blog sur Laravel ici ! Eh bien Laravel et Nuxt peuvent faire bon ménage même si a priori la cohabitation peut sembler délicate. Heureusement quelqu’un s’est attelé à la tâche et nous a pondu de supers packages ! On va juste attendre qu’ils soient actualisé pour la version 2. Mais de toute façon on peut très bien faire les développements séparés : SPA avec Nuxt et l’API avec Laravel ou Lumen.




Comprendre Vue.js : le routeur

Dans cet article je vous propose de prolonger l’exemple du quiz en l’enrichissant. Jusque là on a proposé un seul questionnaire, ce qui est limité. Dans une situation réaliste on aurait le choix entre plusieurs questionnaires. Du coup on va se  retrouver avec deux page : une pour le choix du questionnaire et l’autre pour répondre aux questions. Ça nous donne l’occasion de découvrir le routeur de Vue qui est bien pratique !

Le code final pour cet article est téléchargeable ici.

On crée le projet

On va encore utiliser Vue Cli pour créer le projet. Je ne vais pas trop détailler à nouveau le processus parce que je l’ai fait dans les précédents articles. Par exemple avec l’interface graphique, on va choisir le nom quiz3 :

Choisir le mode manuel :

Là en plus de babel et du linter on prend Vuex et le routeur :

On crée le projet en prenant une configuration de base pour le linter dont je ne vous ai pas encore parlé.

Ensuite on ajoute Bootstrap Vue comme dans les précédents articles :

Vous devez donc avoir ces 4 dépendances principales :

En ce qui concerne le serveur pour l’API vous avez dû l’installer globalement dans le précédent article. Il doit donc être encore disponible.

On a la page d’accueil habituelle mais avec deux liens supplémentaires :

Si on clique sur about :

Notez la valeur de l’url. Vous avez peut-être noté que pour la page d’accueil l’url est : http://localhost:8080/#/.

Le routeur de Vue sait quel composant utiliser selon la valeur dans l’url qui suit #/.

Pour le code on a cette structure :

On a quelques nouveautés.

Un nouveau fichier apparaît, router.js, et comme vous devez vous en douter c’est là qu’on va avoir le code du routeur.

On voit aussi un nouveau dossier, views, avec deux fichiers. Alors ce sont des composants comme les autres, la seule différence réside dans une question d’organisation. On pourrait se contenter de tout mettre dans le dossier components. Mais on nous propose cette architecture pour avoir un composant pour chacune des urls du routeur dans le dossier views. Ensuite ces composants peuvent très bien utiliser d’autres composants qui eux sont dans le dossier components. On va pour cet article se plier à cette organisation même si c’est un peu lourd par rapport à la simplicité de l’application.

La configuration

Regardons le code du fichier main.js :

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

On retrouve ce qu’on a déjà vu avec en plus le routeur importé et déclaré de la même manière que Vuex (store).

Comme on utilise Bootstrap Vue on va ajouter le code que vous connaissez bien maintenant :

...

import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

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

...

Et on est bon pour la configuration !

Le routeur

Vous pouvez lire la documentation complète ici.

Pour le routeur c’est la découverte, voyons le code de router.js :

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
    }
  ]
})

Pour chaque route on définit : le chemin (ce qui apparaît après le #), optionnellement un nom, et le composant à utiliser. Je n’évoquerai pas la particularité de la deuxième route pour rester simple (on diffère le chargement du composant). On pourrait écrire :

{
  path: '/about',
  name: 'about',
  component: About
}

En important évidemment le composant…

Regardez le code du template dans le composant principal App.vue :

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

On voit qu’on utilise deux composants :

  • router-link : génère par défaut une balise <a>, la propriété to prend la valeur de la route, c’est ce qui génère ces deux liens :

  • router-view : le composant sélectionné par la route est généré à cet emplacement, pour la page d’accueil (route ‘/’) c’est le composant Home qui est généré (dans le dossier views) :
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'home',
  components: {
    HelloWorld
  }
}
</script>

On a affichage du logo et appel du composant Helloworld qui lui affiche tout le reste.

Pour la route about c’est le composant About qui est tout simple pour l’exemple :

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

Je pense que vous avez compris le principe de base de ce routeur.

Si vous parcourez la documentation, ce que je vous conseille vivement, vous verrez qu’il est très riche en possibilités !

Maintenant qu’on a un routeur et qu’on a compris comment il fonctionne il faut décider quelle architecture on va mettre en place pour les quizs. je vous propose deux vues qui feront chacune appel à un composant (mais rappelez-vous que les vues sotn en fait des composants) :

On a deux états :

  • choix du quiz : c’est la page d’accueil Home qui utilise le composant Quizs avec la route ‘/’
  • exécution du quiz : c’est la vue Action qui utilise le composant Quiz avec la route ‘/action’

Donc pour le routeur on va avoir ce code :

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Action from './views/Action.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/action',
      name: 'action',
      component: Action
    }
  ]
})

Là la compilation ne va plus fonctionner parce que vous n’avez pas créé le composant Action. Dans un premier temps renommez About en Action pour pouvoir compiler.

On reviendra plus loin sur la route action pour la compléter.

Comme on ne va pas utiliser les liens pour le routage dans le composant App supprimez-les dans le template :

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

Les données

Comme on l’a fait dans le précédent article on va ajouter un fichier db.json à la racine du projet avec ce code pour avoir 2 quizs à disposition :

{
  "quizs" : [
    {
      "nom" : "Culture générale",
      "questions" : [
        {
          "question": "Quel révolutionnaire et grand orateur a déclaré en 1792 : “De l’audace, encore de l’audace, toujours de l’audace.”",
          "answers": [
            "Desmoulin",
            "Danton",
            "Robespierre",
            "Saint Just"
          ],
          "ok": 1
        },
        {
          "question": "Dans quel pays peut-on trouver le mont Elbrouz ?",
          "answers": [
            "Russie",
            "Azerbaïdjan",
            "Géorgie",
            "Iran"
          ],
          "ok": 0
        },
        {
          "question": "Qui a dit “Ich bin ein Berliner” ?",
          "answers": [
            "Bismarck",
            "Reagan",
            "De Gaulle",
            "Kennedy"
          ],
          "ok": 3
        }
      ]
    },
    {
      "nom" : "Technique",
      "questions" : [
        {
          "question": "Quelle est la science de base des autres sciences ?",
          "answers": [
            "La biologie",
            "Les mathématiques",
            "La géographie",
            "La chimie"
          ],
          "ok": 1
        },
        {
          "question": "Quelle est l'unité de la capacité d'un condensateur ?",
          "answers": [
            "Le faraday",
            "Le volt",
            "Le pascal",
            "L'hertz"
          ],
          "ok": 0
        },
        {
          "question": "Quelle est la formule de la loi d'Ohm ?",
          "answers": [
            "E = mc²",
            "P = UI",
            "U = RI",
            "P = RI²"
          ],
          "ok": 2
        }
      ]
    }
  ]
}

Vous pouvez démarrer le serveur avec :

json-server db.json

Vérifier à l’adresse http://localhost:3000/quizs que vous recevez bien les données.

On doit maintenant configuer Vuex (donc le fichier store.js) pour récupérer ces données et les rendre disponibles pour l’application.

Comme il nous faut Axios installez-le aussi (avec l’interface graphique ou avec la console npm i axios).

Le code va être pratiquement identique à celui du dernier article :

import Vue from 'vue'
import Vuex from 'vuex'
import Axios from 'axios'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    quizs: []
  },
  mutations: {
    setData(state, data) {
      state.quizs = data;
    }
  },
  actions: {
    async getData(context) {
      let data = (await Axios.get('http://localhost:3000/quizs')).data;
      context.commit("setData", data);
    }
  }
})

Le choix du quiz

On va commencer par régler le code du composant principal App :

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
#app {
  Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

On place juste le résultat du routeur et du style pour toute l’application.

Le routeur nous envoie dans le composant Home pour l’url de base. Par défaut on a l’appel du composant Helloworld qu’on supprime. A la place on va appeler un composant Quizs qu’on a pas encore créé :

<template>
  <div class="home">
    <img alt="logo" class="mb-4" src="../assets/quiz.png">
    <quizs />
  </div>
</template>

<script>
import Quizs from '@/components/Quizs.vue'

export default {
  name: 'home',
  components: {
    Quizs
  }
}
</script>

J’ai aussi supprimé le titre et changé le nom du logo pour quiz, trouvez un joli logo pour l’application !

Donc on crée un fichier Quizs.vue :

Avec ce code :

<template>
  <div class="container">
    <b-card header="Choisissez un quiz" header-tag="header">
      <b-list-group>
        <b-list-group-item 
          button 
          v-for="(item, index) in quizs" 
          :key="item.id"
          @click="action(index)">
          {{ item.nom }}
        </b-list-group-item>
      </b-list-group>
    </b-card>
  </div>
</template>

<script>
export default {
  name: 'quizs',
  methods: {
    action: function(index) {
      this.$router.push({ name: 'action', params: { id: index }})
    }
  },
  computed: {
    quizs () {
      return this.$store.state.quizs;
    }
  },
  created() {
    this.$store.dispatch('getData');
  }
}
</script>

Lorsque le composant est créé (created) on demande les données à Vuex (store) :

created() {
  this.$store.dispatch('getData');
}

Les données sont récupérées ici :

computed: {
  quizs () {
    return this.$store.state.quizs;
  }
},

C’est ce qu’on a déjà vu dans le précédent article. Et on affiche les noms des quizs :

Quand on clique sur une des deux possibilités on appelle la méthode action :

action: function(index) {
  this.$router.push({ name: 'action', params: { id: index }})
}

On accède au routeur avec this.$router. La méthode push permet d’appeler une route, ici action. Mais pour identifier le quiz on ajoute un paramètre id qui est l’index du quiz.

On va modifier le code du routeur (router.js) pour en tenir compte :

path: '/action/:id',
name: 'action',
component: Action

Le paramètre arrive après un double point.

Pour le moment on aboutit juste à cette page :

Parce qu’on a juste changé le com du composant, on va donc maintenant s’occuper de cette partie.

Action !

Pour le composant Action on va se contenter de ce code :

<template>
  <div>
    <quiz />
  </div>
</template>

<script>
import Quiz from '@/components/Quiz.vue'

export default {
  name: 'action',
  components: {
    Quiz
  }
}
</script>

En fait on se contente d’appeler un composant Quiz. On crée ce composant :

Avec ce code :

<template>
  <div class="container">
    <h1 class="mb-4">{{ nom }}</h1>
    <b-alert v-if="fin" show>Votre score est : {{ score }} / {{ questions.length }}</b-alert>
    <b-card :header="questions[index].question"
            header-tag="header">
      <b-list-group>
        <b-list-group-item 
          button 
          v-for="(item, index) in questions[index].answers" 
          :key="item.id"
          @click="action(index)"
          :variant="variants[index]">
          {{ item }}
        </b-list-group-item>
      </b-list-group>
      <b-button v-if="fin" @click="recommencer" class="mt-4">Recommencer !</b-button>
      <b-button v-if="fin" to="/" class="mt-4 ml-2">Choisir un autre quiz !</b-button>
      <b-button v-if="voirReponse && !fin" @click="continuer" class="mt-4">Continuer !</b-button>
    </b-card>
  </div>
</template>

<script>
export default {
  name: 'quiz',
  data: function () {
    return {
        id: 0,
        fin: false,
        index: 0,
        score: 0,
        variants: [...Array(4)],
        voirReponse: false,
    }
  },
  methods: {
    action: function(index) {
      if(index == this.questions[this.index].ok) {
        this.score++;
      } else {
        this.variants[index] = 'danger';
      }
      this.voirReponse = true;
      this.variants[this.questions[this.index].ok] = 'success';
      if(this.index == this.questions.length - 1) {
        this.fin = true;
      }
    },
    recommencer: function() {
      this.voirReponse = this.fin = this.index = this.score = 0;
      this.variants = [...Array(4)];
    },
    continuer: function() {
      this.voirReponse = false;
      this.variants = [...Array(4)];
      this.index++;    
    }
  },
  computed: {
    nom () {
        return this.$store.state.quizs[this.id].nom;
    },
    questions () {
        return this.$store.state.quizs[this.id].questions;
    }
  },
  created() {
    this.id = this.$route.params.id;
  }
}
</script>

On récupère l’id dans la méthode created :

created() {
  this.id = this.$route.params.id;
}

On sait ainsi quel quiz est actif. Le reste du code n’apporte aucune nouveauté.

On peut afficher le nom du Quiz et les questions :

A la fin du quiz on a maintenant les deux possibilités :

Pour le bouton de choix d’un autre quiz voici le code :

<b-button v-if="fin" to="/" class="mt-4 ml-2">Choisir un autre quiz !</b-button>

La propriété to permet créer facilement un lien pour le routeur.

Conclusion

On a vue dans cet article le principe du routeur de Vue. Vous pouvez consulter la documentation officielle pour plus de précision.




Comprendre Vue.js : Vuex

Dans le précédent article on a vu la réalisation d’un quiz. Les données (questions et réponses) ont été placées dans la propriété data du composant. C’est simple et efficace. Mais imaginez qu’on ait plusieurs composants qui se partagent ces informations. On n’a pas trop creusé la communication entre composants mais on a vu déjà les props pour transmettre une information du composant parent vers l’enfant. On verra que dans l’autre sens on utilise des événements. Ça peut devenir rapidement assez lourd. Il serait bien de disposer des données dans un endroit accessible pour tous les composants…

La solution préconisée est d’utiliser la librairie Vuex. Elle permet de stocker des données mais pas seulement ! On peut gérer très précisément comment ces données sont lues (accesseurs) et modifiées (mutateurs). On peut aussi définir certaines actions. Enfin on peut facilement déboguer avec les outils de Vue.

On va reprendre le quiz du précédent article et cette fois placer les données dans une instance de Vuex. Pour compléter on ira récupérer ces données sur un serveur.

Le code final pour cet article est téléchargeable ici.

On crée le projet

Vue Cli comporte la possibilité de passer par une interface graphique. Elle en est encore en version beta mais fonctionne bien alors nous allons l’utiliser. Placez-vous dans le dossier de travail et tapez :

vue ui

Vous devriez aboutir sur cette page dans votre navigateur :

Cliquez sur Créer :

Puis sur “Créer un nouveau projet ici” si vous êtes bien dans le dossier désiré, sinon vous pouvez vous déplacer facilement :

J’ai choisi le nom quiz2 et npm. On clique sur “Suivant” :

Là choisissez le mode manuel parce qu’on va ajouter des choses. Cliquez sur “Suivant” :

Là vous activez Vuex et vous cliquez encore sur “Suivant” :

Vous réglez le linter par défaut, puis cliquez “Créer le projet” puis “Continuer sans sauvegarder”. Et là plus qu’à attendre :

Quand c’est terminé vous pouvez voir les plugins installés :

Les dépendances :

Vous avez accès aux actions de base :

Vous pouvez utiliser le bouton serve :

Là vous pouvez lancer la compilation du projet…

Vous avez tous les détails sur le tableau de sortie :

Vous arrivez évidemment sur la page d’accueil classique :

C’est une alternative à l’utilisation de la console !

On va en profiter pour ajouter aussi Bootstrap-vue dans les dépendances :

Le projet

Voyons un peu ce qu’on a sous le capot :

Par rapport au projet par défaut on a le fichier store.js en plus. C’est justement celui de Vuex :

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  }
})

On importe Vue et Vuex. La méthode use de Vue est celle qui est utilisée pour utiliser un plugin. On crée une nouvelle instance de Vuex avec new. Ensuite le module exporte ses données avec state. Il est prévu aussi des propriétés pour les mutateurs et les actions (de la même manière on peut ajouter getters). On a donc le squelette de notre module de gestion des données.

Maintenant si vous ouvrez main.js (je rappelle que c’est le fichier de configuration) :

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

On voit qu’on importe store et qu’on le déclare. Il sera donc disponible dans toute l’application.

Tant qu’on est dans ce fichier on va ajouter ce qu’il faut pour Bootstrap-vue comme on l’a fait dans le précédent article :

import Vue from 'vue'
import App from './App.vue'
import store from './store'

import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);

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

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

Les données

J’ai dit que les données seront gérées par Vuex alors on met le questionnaire dans store.js :

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    questions: [
      {
        question: "Quel révolutionnaire et grand orateur a déclaré en 1792 : “De l’audace, encore de l’audace, toujours de l’audace.”",
        answers: [
          'Desmoulin',
          'Danton',
          'Robespierre',
          'Saint Just'
        ],
        ok: 1
      },
      {
        question: "Dans quel pays peut-on trouver le mont Elbrouz ?",
        answers: [
          'Russie',
          'Azerbaïdjan',
          'Géorgie',
          'Iran'
        ],
        ok: 0
      },
      {
        question: "Qui a dit “Ich bin ein Berliner” ?",
        answers: [
          'Bismarck',
          'Reagan',
          'De Gaulle',
          'Kennedy'
        ],
        ok: 3
      }
    ]
  },
  mutations: {

  },
  actions: {

  }
})

Le composant App

On supprime le composant HelloWorld qui ne nous servira pas.

Dans le composant App on recopie à l’identique le template du dernier article :

<template>
  <div id="app" class="container">
    <h1 class="mb-4">Un petit quiz</h1>
    <b-alert v-if="fin" show>Votre score est : {{ score }} / {{ questions.length }}</b-alert>
    <b-card :header="questions[index].question"
            header-tag="header">
      <b-list-group>
        <b-list-group-item 
          button 
          v-for="(item, index) in questions[index].answers" 
          :key="item.id"
          @click="action(index)"
          :variant="variants[index]">
          {{ item }}
        </b-list-group-item>
      </b-list-group>
      <b-button v-if="fin" @click="recommencer" class="mt-4">Recommencer !</b-button>
      <b-button v-if="voirReponse && !fin" @click="continuer" class="mt-4">Continuer !</b-button>
    </b-card>
  </div>
</template>

On conserve le style par défaut.

Dans le script on va utiliser ce code :

<script>
export default {
  name: 'app',
  data: function () {
    return {
      fin: false,
      index: 0,
      score: 0,
      variants: [...Array(4)],
      voirReponse: false,
    }
  },
  methods: {
    action: function(index) {
      // Test bonne réponse
      if(index == this.questions[this.index].ok) {
        this.score++;
      } else {
        this.variants[index] = 'danger';
      }
      this.voirReponse = true;
      this.variants[this.questions[this.index].ok] = 'success';
      if(this.index == this.questions.length - 1) {
        this.fin = true;
      }
    },
    recommencer: function() {
      this.voirReponse = this.fin = this.index = this.score = 0;
      this.variants = [...Array(4)];
    },
    continuer: function() {
      this.voirReponse = false;
      this.variants = [...Array(4)];
      this.index++;
    }
  },
  computed: {
    questions () {
      return this.$store.state.questions;
    }
  }
}
</script>

On a comme différences :

  • la suppression des questions dans data
  • l’ajout de la propriété calculée (computed) questions :
computed: {
  questions () {
    return this.$store.state.questions;
  }
}

On récupère les données de Vuex avec this.$store.state. C’est très simple !

Et maintenant on se retrouve avec le même fonctionnement que pour le précédent article :

On s’est donné bien du mal pour avoir le même résultat 🙂

Un web service RESTful

On va maintenant créer un web service pour nous fournir les questions, ce qui sera déjà plus réaliste. On pourrait le créer en PHP mais puisqu’on est dans Javascript autant continuer dans le style.

On va installer globalement json-server :

npm install -g json-server

On va ainsi pouvoir ajouter facilement un web service RESTful.

Il suffit de créer un fichier db.json (ou un autre nom quelconque) à la racine du projet :

Avec les données bien formées en JSON :

{
  "questions" : [
    {
      "question": "Quel révolutionnaire et grand orateur a déclaré en 1792 : “De l’audace, encore de l’audace, toujours de l’audace.”",
      "answers": [
        "Desmoulin",
        "Danton",
        "Robespierre",
        "Saint Just"
      ],
      "ok": 1
    },
    {
      "question": "Dans quel pays peut-on trouver le mont Elbrouz ?",
      "answers": [
        "Russie",
        "Azerbaïdjan",
        "Géorgie",
        "Iran"
      ],
      "ok": 0
    },
    {
      "question": "Qui a dit “Ich bin ein Berliner” ?",
      "answers": [
        "Bismarck",
        "Reagan",
        "De Gaulle",
        "Kennedy"
      ],
      "ok": 3
    }
  ]
}

Ensuite on lance le serveur :

json-server db.json

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/questions

  Home
  http://localhost:3000

On peut maintenant aller chercher les questions à l’adresse http://localhost:3000/questions.

Axios

Vue ne propose rien pour faire des requêtes HTTP, il faut donc utiliser une autre librairie. Classiquement c’est Axios qui est utilisé. On commence par l’installer (vous pouvez aussi passer par l’interface graphique) :

npm i axios

Et on va l’importer dans store.js :

import Vue from 'vue'
import Vuex from 'vuex'
import Axios from 'axios'

On va modifier le code pour envoyer la requête HTTP et récupérer la réponse :

export default new Vuex.Store({
  state: {
    questions: []
  },
  mutations: {
    setData(state, data) {
      state.questions = data;
    }
  },
  actions: {
    async getData(context) {
      let data = (await Axios.get('http://localhost:3000/questions')).data;
      context.commit("setData", data);
    }
  }
})

On commence par supprimer les données qu’on avait mises dans questions.

On crée un mutateur setData pour renseigner les questions. C’est en effet le seul moyen pour modifier des données avec Vuex. On reçoit state comme premier argument et on peut passer d’autres arguments, ici on passe les données.

On crée une action asynchrone (async) et on attend (await) la réponse. Lorsqu’on la reçoit on invoque le mutateur avec contect.commit.

Maintenant que c’est en place il ne nous reste plus qu’à appeler cette fonction getData.

Les étapes d’une application

Une application Vue passe par 3 étapes :

  • created (initialisations)
  • mounted (création du DOM)
  • destroyed (suppression de l’application)

On dispose de 6 événements :

  • beforeCreated
  • created
  • beforeMounted
  • mounted
  • beforeDestroyed
  • destroyed

On va utiliser l’événement created dans le composant App :

<script>
export default {
  ...
  created() {
    this.$store.dispatch('getData');
  }
}
</script>

Là on appelle la méthode getData avec dispatch et normalement ça devrait fonctionner 🙂

Les outils

Vous pouvez observer ce qui se passe dans Vuex avec les outils de développement :

Et évidemment voir tout ce qui se passe aussi dans le composant :

Et suivre les requêtes HTTP :

Conclusion

Dans cet article on a vu comment mettre en œuvre Vuex pour centraliser les données dans une application Vue et ainsi les rendre accessibles à partir de tous les composants. On a également vu l’utilisation d’Axios pour récupérer des données sur un serveur. On a également utilisé une interface graphique de Vue Cli comme alternative à la console.

 

 




Comprendre Vue.js : un quiz

Poursuivons notre découverte de Vue.js avec une nouvelle petite application. Cette fois il va être question d’un quiz avec donc question et possibilités de réponses, ce qui va nous permettre de voir comment Vue gère les listes de données.

Le code final pour cet article est téléchargeable ici.

On crée le projet

Toujours avec le Cli on crée un nouveau projet :

vue create quiz

Vérifiez que le projet est correctement créé en lançant le serveur :

npm run serve

Si vous avez la page d’accueil alors tout va bien on peut poursuivre…

Cette fois on va commencer par supprimer le composant HelloWorld :

Et pour que ça fonctionne encore on va modifier le code du composant App :

<template>
  <div id="app">
    <h1>Un quiz ?</h1>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
#app {
  Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

J’ai supprimé les références au composant HelloWorld, le logo, et ajouté un titre :

Bootstrap

On va encore ajouter Bootstrap pour gérer l’apparence mais cette fois, au lieu de charger la librairie directement, on va faire appel à une librairie qui a transformé les composants de Bootstrap en composants Vue. Il existe des template pour avoir directement une application toute prête avec Vue Cli mais on va plutôt détailler l’installation.

D’abord on installe la librairie :

npm i bootstrap-vue

Et on charge main.js de charger tout ça :

import Vue from 'vue'
import App from './App.vue'

import BootstrapVue from 'bootstrap-vue'

Vue.use(BootstrapVue);

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

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Vous trouvez une documentation détaillée de la librairie ici :

Pour vous assurez que ça fonctionne ajoutez une alerte dans le template :

<b-alert show>Une alerte</b-alert>

Vous l’avez bien ? Alors c’est parfait on peut poursuivre…

On va faire un petit essai de look pour notre quiz avec ce template :

<template>
  <div id="app" class="container">
    <h1 class="mb-4">Un petit quiz</h1>
    <b-card header="Quel révolutionnaire et grand orateur a déclaré en 1792 : “De l’audace, encore de l’audace, toujours de l’audace.”"
            header-tag="header">
      <b-list-group>
        <b-list-group-item button>Desmoulin</b-list-group-item>
        <b-list-group-item button>Danton</b-list-group-item>
        <b-list-group-item button>Robespierre</b-list-group-item>
        <b-list-group-item button>Saint Just</b-list-group-item>
      </b-list-group>
    </b-card>
  </div>
</template>

On obtient cet aspect :

Vous voyez que cette librairie nous permet de mettre en œuvre Bootstrap avec une syntaxe simple et lisible.

Maintenant il faut nous occuper de la gestion de ce quiz. On va avoir évidemment plusieurs questions qui vont se succéder et à l’arrivée le bilan des réponses.

Les données

Pour ne pas me compliquer la vie je vais prendre 3 questions d’un site de culture générale :

<script>
export default {
  name: 'app',
  data: function () {
    return {
      index: 0,
      score: 0,
      questions: [
        {
          question: "Quel révolutionnaire et grand orateur a déclaré en 1792 : “De l’audace, encore de l’audace, toujours de l’audace.”",
          answers: [
            'Desmoulin',
            'Danton',
            'Robespierre',
            'Saint Just'
          ],
          ok: 1
        },
        {
          question: "Dans quel pays peut-on trouver le mont Elbrouz ?",
          answers: [
            'Russie',
            'Azerbaïdjan',
            'Géorgie',
            'Iran'
          ],
          ok: 0
        },
        {
          question: "Qui a dit “Ich bin ein Berliner” ?",
          answers: [
            'Bismarck',
            'Reagan',
            'De Gaulle',
            'Kennedy'
          ],
          ok: 3
        }
      ]
    }
  },
}
</script>

Donc on va avoir :

  • index : pour savoir où on en est des questions
  • score : pour savoir où en est le score
  • questions : les questions à poser avec pour chacune :
    • la question
    • les réponses possibles
    • l’index de la bonne réponse

Gestion d’une liste d’éléments

Vue propose la directive v-for pour gérer les listes de données. c’est une directive puissante et simple qu’on va utiliser pour afficher les réponses. En ce qui concerne le texte de la question c’est une simple liaison. Voici le template modifier pour afficher la question et ses réponses :

<template>
  <div id="app" class="container">
    <h1 class="mb-4">Un petit quiz</h1>
    <b-card :header="questions[index].question"
            header-tag="header">
      <b-list-group>
        <b-list-group-item 
          button 
          v-for="item in questions[index].answers" 
          :key="item">
          {{ item }}
        </b-list-group-item>
      </b-list-group>
    </b-card>
  </div>
</template>

Remarquez comment on renseigne un attribut avec une liaison sur le nom de l’attribut, ici header pour la question.

Quant aux réponses possibles la directive v-for fait tranquillement le travail. Il est obligatoire ici d’ajouter une clé (key) pour avoir un identifiant unique pour chaque item de la boucle.

On obtient le même affichage que précédemment mais maintenant créé dynamiquement :

Maintenant on aura une mise à jour automatique de la question et de ses réponses en changeant seulement la valeur de index.

Action

Quand on clique sur une réponse il faut :

  • évaluer cette réponse pour savoir si c’est la bonne et dans ce cas incrémenter le score
  • passer à la question suivant et s’il n’y en a plus afficher les résultats

On va équiper les réponses d’un événement sur le clic :

<b-list-group-item 
  button 
  v-for="(item, index) in questions[index].answers" 
  :key="item.id"
  @click="action(index)">
  {{ item }}
</b-list-group-item>

Remarquez que dans la directive v-for j’ai ajouté un index pour identifier les réponses. Comme ça on peut transmettre cet index comme argument à la méthode action.

On crée donc la propriété pour les méthodes dans le composant avec la méthode action :

<script>
export default {
  name: 'app',
  data: function () {
    return {
      fin: false,
      
      ...

  methods: {
    action: function(index) {
      // Test bonne réponse
      if(index == this.questions[this.index].ok) {
        this.score++;
      }
      // Test fin de quiz
      if(this.index == this.questions.length - 1) {
        this.fin = true;
      } else {
        this.index++;
      }
    }
  }
}
</script>

J’ai aussi ajouté une variable fin qui est false au départ et qui devient true à la fin du quiz.

On finit en ajoutant une barre d’alerte dans le template :

<h1 class="mb-4">Un petit quiz</h1>
<b-alert v-if="fin" show>Votre score est : {{ score }} / {{ questions.length }}</b-alert>

On la contrôle avec une directive v-if et la variable fin qu’on a créée plus haut.

On lance pour voir si ça fonctionne…

Après ma première réponse je passe bien à la réponse suivante :

Et à la fin j’ai bien l’alerte avec mon score :

Comme je connaissais les réponses j’ai une bonne note 🙂

Gestion du quiz

Le quiz fonctionne mais arrivé à la fin on ne peut plus rien faire si ce n’est relancer l’application pour le refaire. ce n’est pas très élégant alors on va ajouter un bouton pour proposer de recommencer :

  <b-button v-if="fin" @click="recommencer" class="mt-4">Recommencer !</b-button>
</b-card>

On utilise la même directive v-if que pour l’alerte parce que la condition est la même.

On intercepte l’événement clic en appelant une méthode recommencer. On ajoute cette méthode :

methods: {
  ...
  recommencer: function() {
    this.fin = this.index = this.score = 0;
  },

Là on se contente de réinitialiser les variables et c’est reparti !

Les bonnes réponses

On peut améliorer notre quiz en indiquant à chaque fois la bonne réponse. Mais du coup il nous faut aussi un bouton pour passer à la question suivante…

On commence par ajouter deux variables dans le composant :

data: function () {
  return {
    ...
    variants: [...Array(4)],
    voirReponse: false,

On initialise un tableau avec le nombre de questions. La variable voirReponse nous permettra de savoir si on en est à choisir une réponse ou à l’affichage de la réponse.

On réorganise les méthodes et on ajoute une méthode continuer :

methods: {
  action: function(index) {
    // Test bonne réponse
    if(index == this.questions[this.index].ok) {
      this.score++;
    } else {
      this.variants[index] = 'danger';
    }
    this.voirReponse = true;
    this.variants[this.questions[this.index].ok] = 'success';
    if(this.index == this.questions.length - 1) {
      this.fin = true;
    }
  },
  recommencer: function() {
    this.voirReponse = this.fin = this.index = this.score = 0;
    this.variants = [...Array(4)];
  },
  continuer: function() {
    this.voirReponse = false;
    this.variants = [...Array(4)];
    this.index++;  
  }
}

Il ne nous reste plus qu’à prévoir un bouton pour continuer :

<b-button v-if="voirReponse && !fin" @click="continuer" class="mt-4">Continuer !</b-button>

Et ça devrait rouler !

Si on choisit la bonne réponse elle apparaît en vert et on a le bouton pour passer à la question suivante :

Si on a une mauvaise réponse elle apparaît en rouge et la bonne apparaît en vert :

Ça commence à avoir de l’allure !

Conclusion

On a vu pas mal de choses dans cet article : l’utilisation d’un librairie pour utiliser facilement Bootstrap, comment gérer une liste de données, comment rendre conditionnel l’affichage d’éléments, comment jouer avec les liaisons et les événements…