Page dynamique

image_pdfimage_print

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 !

5 commentaires sur “Page dynamique

  1. Personnellement j’aurais également signalé que en DB c’est exactement le même principe.
    De même pour ceux qui cherchent régulièrement comment alimenter un menu déroulant au départ d’un autre menu déroulant, nous avons toute la procédure dans ton exemple 🙂
    Merci.

Laisser un commentaire