Changer le framework CSS bis

Il y a un peu plus d’un an j’ai publié un article qui montre comment remplacer Bootstrap qui est utilisé par défaut par Materialize. Un commentaire récent pour cet article m’a montré que les choses évoluent très vite et que ce que j’avais dit était devenu obsolète. Entre temps Laravel a changé et Materialize est enfin passé à la version 1. Je vous présente donc dans le présent article la procédure actualisée.

Pour vous faciliter la vie le projet complet est téléchargeable ici.

Installation par défaut

Partez d’une nouvelle installation de Laravel :

composer create-project --prefer-dist laravel/laravel laravel5

Puis générez les vue pour l’authentification :

php artisan make:auth

Renseignez le fichier .env pour la connexion à la base puis générez les migrations :

php artisan migrate

Si tout se passe bien vous avez 3 tables :

Ensuite installez les modules avec npm :

npm install

Lancez en mode développement pour voir si ça fonctionne :

npm run dev

Vous devez avoir la compilation des assets dans les fichiers public/css/app.css et public/js/app.js comme c’est prévu dans le fichier webpack.mix.js :

mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');

Pour mémoire les sources des assets sont ici :

Pour le CSS on voit que dans le fichier resources/sass/app.scss on importe bootstrap :

@import '~bootstrap/scss/bootstrap';

Et qu’on le modifie un peu en chargeant de nouvelles variables :

@import "variables";

Pour le Javascript on voit que dans le fichier resources/assets/js/bootstrap.js on charge bootstrap :

require('bootstrap-sass');

D’autre part on charge aussi lodash, jQuery et Vue.js avec un composant par défaut mais ce n’est pas l’objet du présent article.

Voilà donc un petit état des lieux à l’installation de Laravel et on se retrouve pour les vues de l’authentification avec un aspect typique de Bootstrap :

On va maintenant procéder aux modifications pour utiliser non plus Bootstrap mais Materialize que j’aime bien. Mais les explications restent valables pour n’importe quel framework CSS.

On passe à Materialize

L’intendance

Sur le site de npm on cherche le module :

Et comme on veut ce module uniquement pour le développement on va utiliser cette syntaxe :

npm install materialize-css --save-dev

Si tout va bien le fichier package.json est actualisé :

"devDependencies": {
    ...
    "materialize-css": "^1.0.0",
    ...
}

On va maintenant mettre à jour le fichier resources/sass/app.scss pour importer Materialize plutôt que Bootstrap :

// Fonts
//@import url('https://fonts.googleapis.com/css?family=Nunito');
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");

// Variables
//@import 'variables';

// Bootstrap
//@import '~bootstrap/scss/bootstrap';

//Materialize
@import "~materialize-css/sass/materialize.scss";

.card { margin-top: 80px; };

Et pour le Javascript ça se passe dans le fichier resources/js/bootstrap.js, on va se contenter de ça :

window.$ = window.jQuery = require('jquery');
require('materialize-css');

On va lancer en mode développement avec actualisation automatique :

npm run watch

Si on regarde maintenant la page de login on va lui trouver évidemment un salle aspect. C’est normal parce qu’on a les classes de Bootstrap déclarées et qu’on a plus ce framework disponible. Il nous reste maintenant à modifier les vues avec les classes de Materialize.

Layout

Voici la nouvelle vue resources/views/layouts/app.blade.php :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Styles -->
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    @yield('css')
</head>
<body>
    <div id="app">

        <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
            @csrf
        </form>

        @auth
            <ul id="dropdown1" class="dropdown-content">
                <li><a href="{{ route('logout') }}" class="logout">{{ __('Logout') }}</a></li>
            </ul>
        @endauth

        <nav>
            <div class="nav-wrapper">
                <a href="{{ url('/') }}" class="brand-logo">&nbsp{{ config('app.name', 'Laravel') }}</a>
                <a href="#" data-target="mobile-demo" class="sidenav-trigger"><i class="material-icons">menu</i></a>
                @guest
                    <ul class="right hide-on-med-and-down">
                        <li><a href="{{ route('login') }}">{{ __('Login') }}</a></li>
                        <li><a href="{{ route('register') }}">{{ __('Register') }}</a></li>
                    </ul>
                @else
                    <ul class="right hide-on-med-and-down">
                        <li><a class="dropdown-trigger" href="#!" data-target="dropdown1">{{ Auth::user()->name }}<i class="material-icons right">arrow_drop_down</i></a></li>
                    </ul>
                @endguest
            </div>
        </nav>


        <ul class="sidenav" id="mobile-demo">
            @guest
                <li><a href="{{ route('login') }}">{{ __('Login') }}</a></li>
                <li><a href="{{ route('register') }}">{{ __('Register') }}</a></li>
            @else
                <li><a href="{{ route('logout') }}" class="logout">{{ __('Logout') }}</a></li>
            @endguest
        </ul>

        @yield('content')
    </div>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}"></script>

    <script>
        $(document).ready(function(){
            $('.sidenav').sidenav();
            $('.dropdown-trigger').dropdown();
            $('.logout').click(function(e) {
                e.preventDefault();
                $('#logout-form').submit();
            });
        });
    </script>
</body>
</html>

J’ai changé essentiellement la barre de navigation.

Il faut maintenant modifier les autres vues…

Login

Voici la nouvelle vue de login (resources/views/auth/login.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

    <div class="row">
        <div class="col s12 m10 offset-m1 l8 offset-l2">
            <div class="card">
            <form  method="POST" action="{{ route('login') }}">
                <div class="card-content">
                    {{ csrf_field() }}
                    <span class="card-title">{{ __('Login') }}</span>

                    <hr>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">mail</i>
                            <input id="email" type="email" name="email" value="{{ old('email') }}" class="{{ $errors->has('email') ? 'invalid' : '' }}" required autofocus>
                            <label for="email">{{ __('E-Mail Address') }}</label>
                            <span class="red-text">{{ $errors->has('email') ? $errors->first('email'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">lock</i>
                            <input id="password" type="password" name="password" class="{{ $errors->has('password') ? 'invalid' : '' }}" required>
                            <label for="password">{{ __('Password') }}</label>
                            <span class="red-text">{{ $errors->has('password') ? $errors->first('password'): '' }}</span>
                        </div>
                    </div>

                    <p>
                        <label>
                            <input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
                            <span>{{ __('Remember Me') }}</span>
                        </label>
                    </p>

                    <p>
                        <button class="btn waves-effect waves-light right" type="submit" name="action">{{ __('Login') }}
                            <i class="material-icons right">lock_open</i>
                        </button>
                    </p>

                    <br>

                </div>
                <div class="card-action">
                    @if (Route::has('password.request'))
                        <a href="{{ route('password.request') }}">{{ __('Forgot Your Password?') }}</a>
                    @endif
                </div>
            </form>
        </div>
        </div>
    </div>
</div>
@endsection

Avec cet aspect :

C’est quand même présentable !

Register

Voici la nouvelle vue d’enregistrement (resources/views/auth/register.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

    <div class="row">
        <div class="col s12 m10 offset-m1 l8 offset-l2">
            <div class="card">
            <form  method="POST" action="{{ route('register') }}">
                <div class="card-content">
                    {{ csrf_field() }}
                    <span class="card-title">{{ __('Register') }}</span>

                    <hr>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">person</i>
                            <input id="name" type="text" name="name" value="{{ old('name') }}" class="{{ $errors->has('name') ? 'invalid' : '' }}" required autofocus>
                            <label for="email">{{ __('Name') }}</label>
                            <span class="red-text">{{ $errors->has('name') ? $errors->first('name'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">mail</i>
                            <input id="email" type="email" name="email" value="{{ old('email') }}" class="{{ $errors->has('email') ? 'invalid' : '' }}" required>
                            <label for="email">{{ __('E-Mail Address') }}</label>
                            <span class="red-text">{{ $errors->has('email') ? $errors->first('email'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">lock</i>
                            <input id="password" type="password" name="password" class="{{ $errors->has('password') ? 'invalid' : '' }}" required>
                            <label for="password">{{ __('Password') }}</label>
                            <span class="red-text">{{ $errors->has('password') ? $errors->first('password'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">lock</i>
                            <input id="password-confirm" type="password" name="password_confirmation" required>
                            <label for="password-confirm">{{ __('Confirm Password') }}</label>
                        </div>
                    </div>

                    <p>
                        <button class="btn waves-effect waves-light" type="submit" name="action">{{ __('Register') }}
                            <i class="material-icons right">create</i>
                        </button>
                    </p>

                </div>
            </form>
        </div>
        </div>
    </div>
</div>
@endsection

Avec cet aspect :

Email

Voici la nouvelle vue de demande de renouvellement du mot de passe (resources/views/auth/passwords/email.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

    @if (session('status'))
        <div class="card">
            <div class="card green darken-1">
                <div class="card-content white-text">
                    {{ session('status') }}
                </div>
            </div>
        </div>
    @endif

    <div class="row">
        <div class="col s12 m10 offset-m1 l8 offset-l2">
            <div class="card">

            <form  method="POST" action="{{ route('password.email') }}">
                <div class="card-content">
                    {{ csrf_field() }}
                    <span class="card-title">{{ __('Reset Password') }}</span>

                    <hr>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">mail</i>
                            <input id="email" type="email" name="email" value="{{ old('email') }}" class="{{ $errors->has('email') ? 'invalid' : '' }}" required autofocus>
                            <label for="email">{{ __('E-Mail Address') }}</label>
                            <span class="red-text">{{ $errors->has('email') ? $errors->first('email'): '' }}</span>
                        </div>
                    </div>

                    <p>
                        <button class="btn waves-effect waves-light right" type="submit" name="action">{{ __('Send Password Reset Link') }}
                            <i class="material-icons right">lock_open</i>
                        </button>
                    </p>

                    <br><br>

                </div>
            </form>
        </div>
        </div>
    </div>
</div>
@endsection

Avec cet aspect :

Reset

Voici la nouvelle vue de de renouvellement du mot de passe (resources/views/auth/passwords/reset.blade.php) :

@extends('layouts.app')

@section('content')
<div class="container">

    <div class="row">
        <div class="col s12 m10 offset-m1 l8 offset-l2">
            <div class="card">
            <form  method="POST" action="{{ route('password.update') }}">
                <div class="card-content">
                    {{ csrf_field() }}
                    <input type="hidden" name="token" value="{{ $token }}">

                    <span class="card-title">{{ __('Reset Password') }}</span>

                    <hr>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">mail</i>
                            <input id="email" type="email" name="email" value="{{ old('email') }}" class="{{ $errors->has('email') ? 'invalid' : '' }}" required>
                            <label for="email">{{ __('E-Mail Address') }}</label>
                            <span class="red-text">{{ $errors->has('email') ? $errors->first('email'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">lock</i>
                            <input id="password" type="password" name="password" class="{{ $errors->has('password') ? 'invalid' : '' }}" required>
                            <label for="password">{{ __('Password') }}</label>
                            <span class="red-text">{{ $errors->has('password') ? $errors->first('password'): '' }}</span>
                        </div>
                    </div>

                    <div class="row">
                        <div class="input-field col s12">
                            <i class="material-icons prefix">lock</i>
                            <input id="password-confirm" type="password" name="password_confirmation" required>
                            <label for="password-confirm">{{ __('Confirm Password') }}</label>
                        </div>
                    </div>

                    <p>
                        <button class="btn waves-effect waves-light" type="submit" name="action">{{ __('Reset Password') }}
                            <i class="material-icons right">lock_open</i>
                        </button>
                    </p>

                </div>
            </form>
        </div>
        </div>
    </div>
</div>
@endsection

Avec cet aspect :

Home

Pour finir maintenant le code pour la vue resources/views/home.blade.php :

@extends('layouts.app')

@section('content')
<div class="container">
  <div class="row">
    @if (session('status'))
        <div class="card green darken-1">
            <div class="card-content white-text">
                {{ session('status') }}
            </div>
        </div>
    @endif
      <div class="card red lighten-2">
        <div class="card-content white-text">

            <span class="card-title">Dashboard</span>

            You are logged in!
        </div>
      </div>
    </div>
</div>
@endsection

Avec cet aspect sur mobile :

La vérification de l’email

Laravel offre la possibilité de faire une vérification de l’email lorsque quelqu’un s’enregistre. Il faut le préciser dans les routes de l’authentification :

Auth::routes(['verify' => true]);

Ensuite on définit les routes à protéger, par exemple celle de la vue home :

Route::get('/home', 'HomeController@index')->name('home')->middleware('verified');

Enfin il faut dans le modèle User implémenter l’interface :

class User extends Authenticatable implements MustVerifyEmail

On va aussi adapter la vue resources/views/auth/verify.blade.php pour Materialize :

@extends('layouts.app')

@section('content')
<div class="container">
  <div class="row">
    @if (session('resent'))
        <div class="card green darken-1">
            <div class="card-content white-text">
                {{ __('A fresh verification link has been sent to your email address.') }}
            </div>
        </div>
    @endif
    <div class="card">
        <div class="card-content">
            <div class="card-title">{{ __('Verify Your Email Address') }}</div>
            <hr>
            {{ __('Before proceeding, please check your email for a verification link.') }}
            {{ __('If you did not receive the email') }}, <a href="{{ route('verification.resend') }}">{{ __('click here to request another') }}</a>.
        </div>
      </div>
    </div>
</div>
@endsection

Avec cet aspect :

Vous voyez que le changement de framework CSS n’est pas bien difficile mais il demande un peu d’attention au niveau de Laravel Mix si on veut profiter de cet outil…




Laravel et MDL

Material Design est un langage visuel mis au point par Google. Concernant la partie Material elle évoque le fait que le visuel se rapproche de la réalité matérielle : le papier, l’encre, les ombres… L’initiative avait pour objectif d’unifier leurs applications et de les rendre homogènes sur tous les supports.

Le Material Design c’est essentiellement des règles de design avec des formes simple et lisibles et des effets visuels pour renseigner l’utilisateur de ses actions. On trouve toutes les règles et explications sur le site concerné. C’est une lecture très saine et instructive !

Google nous offre de nombreux outils pour mettre en œuvre le Material Design mais le plus pratique est certainement Materiel Design Lite (MDL). C’est une librairie CSS et Javascript légère et facile à utiliser.

Je me suis dit qu’il serait intéressant de la mettre en œuvre avec Laravel.

Par défaut Laravel arrive avec Bootstrap et JQuery mais on n’est évidemment pas obligés de les utiliser. Dans cet article je montre comment transformer l’installation de base pour utiliser MDL.

Installation de Laravel

Si comme moi vous utilisez Laragon il est facile de créer rapidement une installation toute neuve de Laravel :

Sinon vous devez taper vous-même les commandes :

composer create-project laravel/laravel laravel-mdl --prefer-dist

Si tout se passe bien vous devez obtenir la page d’accueil :

On va ajouter les éléments pour l’authentification :

php artisan make:auth

On va aussi créer une base (Laragon s’en charge aussi). On renseigne le fichier .env pour pointer sur la bonne base :

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-mdl
DB_USERNAME=root
DB_PASSWORD=

Et on lance les migrations par défaut :

php artisan migrate

On va aussi charger les librairies avec npm :

npm install

Voilà on a maintenant un Laravel tout neuf qu’on va pouvoir triturer…

Material Design Lite

On trouve tous les renseignements sur ce site. Pour mettre en œuvre MDL il suffit de faire appel aux librairies :

<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css">
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>

Dans l’appel à la librairie CSS on choisit le thème coloré désiré. Et c’est tout !

Mais pour rendre le CSS plus facile à modifier pour nos besoins il vaut mieux utiliser la version Sass et la compiler avec mix. Pour utiliser la version Sass on nous explique la procédure ici. On ne va pas la suivre exactement pour Laravel.

Il faut télécharger le Web Starter Guide. Le seul fichier qui va nous intéresser est main.css :

Ensuite on télécharge Material Design Lite. Là on va récupérer tout ce qui se trouve dans le dossier src :

Dans l’installation de Laravel on a un dossier avec des fichiers Sass :

On supprime ces deux fichiers et on copie tout le contenu de material-design-lite-1.3.0/src (en ne conservant que les fichiers scss) ainsi que le fichier web-starter-kit-0.6.5/app/styles/main.css. Changez le nom de ce dernier fichier en app.scss. Changez le code de ce fichier pour  celui-ci :

@import "material-design-lite";

html, body {
  Roboto', 'Helvetica', sans-serif;
  margin: 0;
  padding: 0;
}

Vous devez vous retrouver finalement avec ça :

Au niveau du Javascript par défaut Laravel prévoit Bootstrap, JQuery, lodash, popper, et axios. On va se passer de tout ça en se contentant de la librairie Javascript de MDL.

Supprimez tout ce qui se trouve dans resources/js et copiez le fichier material-design-lite-1.3.0/material.js :

Dans le fichier webpack.mix.js changez ainsi le code :

mix.js('resources/js/material.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css');

Il ne reste plus qu’à relancer la compilation en mode développement :

npm run dev

Normalement vous avez un fichier public/css/app.css tout neuf ainsi qu’un nouveau fichier public/js/matrial.js (supprimez l’ancien app.js) :

Mais évidemment si la page d’accueil de Laravel demeure inchangée parce qu’indépendante du CSS global il n’en va pas de même de toutes les autres vues, par exemple pour le login :

Il faut maintenant reprendre toutes les vues pour MDL…

Le layout

On va commencer par mettre à jour le layout (resources/views/layouts/app.blade.php) :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Fonts -->
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">

    <!-- Scripts -->
    <script src="{{ asset('js/material.js') }}" defer></script>

</head>
<body>

    <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
        <header class="mdl-layout__header">
            <div class="mdl-layout__header-row">
            <!-- Title -->
            <span class="mdl-layout-title"><a href="{{ url('/') }}">{{ config('app.name', 'Laravel') }}</a></span>
            <!-- Add spacer, to align navigation to the right -->
            <div class="mdl-layout-spacer"></div>
            <!-- Navigation. We hide it in small screens. -->
            <nav class="mdl-navigation mdl-layout--large-screen-only">
                <!-- Authentication Links -->
                @guest
                    <a class="mdl-navigation__link" href="{{ route('login') }}">{{ __('Login') }}</a>
                    @if (Route::has('register'))
                        <a class="mdl-navigation__link" href="{{ route('register') }}">{{ __('Register') }}</a>
                    @endif
                @else
                    <button class="mdl-button mdl-js-button" id="logout">
                        {{ Auth::user()->name }}
                    </button>
                    <ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right" for="logout">
                        <li class="mdl-menu__item"
                            onclick="event.preventDefault();
                            document.getElementById('logout-form').submit();">
                            {{ __('Logout') }}
                        </li>
                    </ul>
                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                        @csrf
                    </form>
                @endguest
            </nav>
            </div>
        </header>
        <div class="mdl-layout__drawer">
            <span class="mdl-layout-title"><a href="{{ url('/') }}">{{ config('app.name', 'Laravel') }}</a></span>
            <nav class="mdl-navigation">

                <!-- Authentication Links -->
                @guest
                    <a class="mdl-navigation__link" href="{{ route('login') }}">{{ __('Login') }}</a>
                    @if (Route::has('register'))
                        <a class="mdl-navigation__link" href="{{ route('register') }}">{{ __('Register') }}</a>
                    @endif
                @else
                    <a class="mdl-navigation__link" href="{{ route('logout') }}"
                        onclick="event.preventDefault();
                        document.getElementById('logout-form').submit();">
                        {{ __('Logout') }}
                    </a>
                @endguest

            </nav>
        </div>

        @yield('content')

    </div>

</body>
</html>

On utilise le composant layout de MDL. Il permet de créer une barre de navigation :

On a juste un petit souci avec le titre Laravel qu’il faut un peu arranger. On ajoute ces règles dans resources/sass/app.scss :

.mdl-layout-title {
    a {
        color: inherit;
    }
    a:link {
        text-decoration: none;
    }
}

Relancez la compilation (ou mettez en mode watch pour que ce soit automatique).

Maintenant notre barre de navigation est parfaite.

On va créer un layout spécifique pour la partie authentification parce qu’on veut un centrage correct des formulaires, ce qui ne sera pas forcément le cas pour le reste du site :

Avec ce code :

@extends('layouts.app')

@section('content')

<div class="mdl-layout" id="main_layout">
    <main class="mdl-layout__content">
        <div class="page-content">
            @yield('authContent')
        </div>
    </main>
</div>

@endsection

Et ces règles dans resources/sass/app.scss :

#main_layout {

    align-items: center;
    justify-content: center;

    .mdl-layout__content {
        padding: 24px;
        flex: none;
    }

    .mdl-textfield__error {
        visibility: visible;
    }
}

Le login

On va maintenant changer le code pour la vue de login (resources/views/auth/login.blade.php) :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">{{ __('Login') }}</h2>
        </div>
        <div class="mdl-card__supporting-text">
            <form method="POST" action="{{ route('login') }}">
                @csrf

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
                    <label class="mdl-textfield__label" for="email">{{ __('E-Mail Address') }}</label>
                    @if($errors->has('email'))
                        <span class="mdl-textfield__error">{{ $errors->first('email') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="password" id="password" name="password" required>
                    <label class="mdl-textfield__label" for="password">{{ __('Password') }}</label>
                    @if($errors->has('password'))
                        <span class="mdl-textfield__error">{{ $errors->first('password') }}</span>
                    @endif
                </div>

                <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="remember">
                    <input type="checkbox" id="remember" class="mdl-switch__input" {{ old('remember') ? 'checked' : '' }}>
                    <span class="mdl-switch__label">{{ __('Remember Me') }}</span>
                </label>

                <button id="action-button" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored mdl-color-text--white">
                    {{ __('Login') }}
                </button>

            </form>
        </div>
        <div class="mdl-card__actions mdl-card--border">
            @if (Route::has('password.request'))
                <a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" href="{{ route('password.request') }}">
                    {{ __('Forgot Your Password?') }}
                </a>
            @endif
        </div>
    </div>

@endsection

On utilise le composant Cards de MDL. D’autre part dans le formulaire on utilise le composant Text Fields.

Voilà le résultat bien centré dans la page :

On va juste arranger un peu le bouton :

#action-button {
    width: 100%;
    height: 40px;
    min-width: initial;
    margin: 20px 0 10px;
}

Maintenant c’est parfait !

On vérifie que les messages d’erreurs s’affichent bien :

L’enregistrement

On va faire la même chose pour la vue resources/views/auth/register.blade.php :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">{{ __('Register') }}</h2>
        </div>
        <div class="mdl-card__supporting-text">
            <form method="POST" action="{{ route('register') }}">
                @csrf

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="text" id="email" name="name" value="{{ old('name') }}" required autofocus>
                    <label class="mdl-textfield__label" for="name">{{ __('Name') }}</label>
                    @if($errors->has('name'))
                        <span class="mdl-textfield__error">{{ $errors->first('name') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
                    <label class="mdl-textfield__label" for="email">{{ __('E-Mail Address') }}</label>
                    @if($errors->has('email'))
                        <span class="mdl-textfield__error">{{ $errors->first('email') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="password" id="password" name="password" required>
                    <label class="mdl-textfield__label" for="password">{{ __('Password') }}</label>
                    @if($errors->has('password'))
                        <span class="mdl-textfield__error">{{ $errors->first('password') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="password" id="password_confirmation" name="password_confirmation" required>
                    <label class="mdl-textfield__label" for="password_confirmation">{{ __('Confirm Password') }}</label>
                </div>

                <button id="action-button" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored mdl-color-text--white">
                    {{ __('Register') }}
                </button>

            </form>
        </div>
    </div>

@endsection

On en profite pour créer un user :

On y arrive bien mais on a un petit souci avec la vue lorsqu’on est connecté.

Le nom apparaît bien mais il faudrait qu’il soit en blanc :

#logout {
    color: white;
}

Ensuite on change le code de la vue resources/views/home.blade.php :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">Dashboard</h2>
        </div>

        <div class="mdl-card__supporting-text">

            @if (session('status'))
                <div class="mdl-card__supporting-text mdl-color-text--primary">
                    {{ session('status') }}
                </div>
                <hr>
            @endif
            You are logged in!

        </div>
    </div>

@endsection

C’est maintenant plus présentable :

Pour le logout on a un menu déroulant :

Il est ajouté à la barre de navigation avec le composant Menus.

Le renouvellement du mot de passe

Pour le renouvellement du mot de passe on met à jour la vue resources/views/auth/passwords/email.blade.php :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">{{ __('Reset Password') }}</h2>
        </div>

        <div class="mdl-card__supporting-text">
            <form method="POST" action="{{ route('password.email') }}">
                @csrf

                @if (session('status'))
                    <div class="mdl-card__supporting-text mdl-color-text--primary">
                        {{ session('status') }}
                    </div>
                    <hr>
                @endif

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
                    <label class="mdl-textfield__label" for="email">{{ __('E-Mail Address') }}</label>
                    @if($errors->has('email'))
                        <span class="mdl-textfield__error">{{ $errors->first('email') }}</span>
                    @endif
                </div>

                <button id="action-button" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored mdl-color-text--white">
                    {{ __('Send Password Reset Link') }}
                </button>

            </form>
        </div>
    </div>

@endsection

On vérifie que l’email part bien et qu’on a le message :

On change aussi le code de la vue pour modifier le mot de passe (resources/views/auth/passwords/reset.blade.php) :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">{{ __('Reset Password') }}</h2>
        </div>
        <div class="mdl-card__supporting-text">
            <form method="POST" action="{{ route('password.update') }}">
                @csrf

                <input type="hidden" name="token" value="{{ $token }}">

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="email" id="email" name="email" value="{{ old('email') }}" required autofocus>
                    <label class="mdl-textfield__label" for="email">{{ __('E-Mail Address') }}</label>
                    @if($errors->has('email'))
                        <span class="mdl-textfield__error">{{ $errors->first('email') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="password" id="password" name="password" required>
                    <label class="mdl-textfield__label" for="password">{{ __('Password') }}</label>
                    @if($errors->has('password'))
                        <span class="mdl-textfield__error">{{ $errors->first('password') }}</span>
                    @endif
                </div>

                <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
                    <input class="mdl-textfield__input" type="password" id="password_confirmation" name="password_confirmation" required>
                    <label class="mdl-textfield__label" for="password_confirmation">{{ __('Confirm Password') }}</label>
                </div>

                <button id="action-button" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--colored mdl-color-text--white">
                    {{ __('Reset Password') }}
                </button>

            </form>
        </div>
    </div>

@endsection

Parfait !

La vérification de l’email

Laravel offre la possibilité de faire une vérification de l’email lorsque quelqu’un s’enregistre. Il faut le préciser dans les routes de l’authentification :

Auth::routes(['verify' => true]);

Ensuite on définit les routes à protéger, par exemple celle de la vue home :

Route::get('/home', 'HomeController@index')->name('home')->middleware('verified');

Enfin il faut dans le modèle User implémenter l’interface :

class User extends Authenticatable implements MustVerifyEmail

On va aussi adapter la vue resources/views/auth/verify.blade.php pour MDL :

@extends('layouts.auth')

@section('authContent')

    <div class="mdl-card mdl-shadow--6dp">
        <div class="mdl-card__title mdl-color--primary mdl-color-text--white">
            <h2 class="mdl-card__title-text">{{ __('Verify Your Email Address') }}</h2>
        </div>

        <div class="mdl-card__supporting-text">
            {{ __('Before proceeding, please check your email for a verification link.') }}
            {{ __('If you did not receive the email') }}, <a href="{{ route('verification.resend') }}">{{ __('click here to request another') }}</a>.
        </div>
    </div>

@endsection

Et c’est bon pour cette partie aussi !

Conclusion

On a vu qu’il n’est pas si difficile d’utiliser MDL avec Laravel. Les fichiers générés en production sont très légers :

D’autre part on dispose de tous les composants nécessaires pour construire un site plutôt esthétique. Évidemment il faudra ensuite certainement ajouter une librairie Javascript pour gérer efficacement le client, il serait alors dommage d’utiliser JQuery. On peut bien sûr opter pour Vue.js mais aussi se contenter d’une librairie légère comme RE:DOM.

Pour ceux qui n’arriveraient pas à construire l’application ou trop fainéants pour tout faire voici un zip du projet final.




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.