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 !

Print Friendly, PDF & Email

19 commentaires sur “Page dynamique

  1. Bonjour Bestmomo,

    J’ai débuté en Laravel il y a peu et j’ai beaucoup progressé grâce à vos tutos. Alors bravo et merci pour ça.
    Mon site tourne et je cherche à l’améliorer.
    Je voudrais charger dynamiquement le contenu d’un popover ou d’un tooltip. je me demande quelle est la meilleure méthode.

    il y a beaucoup de sites qui traitent de ce sujet mais pas en Laravel. J’ai essayé de les adapter sans succès.

    Merci de bien vouloir m’aiguiller.

    1. salut,

      Ça fait plaisir ce genre de retour sympathique !

      Pour l’aspect dynamique tout dépend comment le code client est construit, il y a aujourd’hui tellement de possibilités qu’on s’y perd un peu. De toute façon il faut voir si les informations sont déjà présentes côté client, ce qui est l’idéal, sinon il faut aller les chercher sur le serveur en Ajax.

      1. Oui, c’est bien vers Ajax que je me suis dirigé puisqu’il faut que je construise une requête sql à partir de ma base.

        Voilà mon code :
        dans mon custom.js
        _____________________________________
        $(document).ready(function(){
        $('[data-toggle="tooltip"]').tooltip();
        $('.popoverEl').popover({
        trigger:'hover',
        title: roomName,
        content: contentRoom,
        html: true
        })
        function roomName(){
        var element = $(this);
        var name = element.attr("name");
        return name;
        };
        function contentRoom(){
        var fetch_data = '';
        var element = $(this);
        var name = element.attr("name");

        $.ajax({
        url:"resources/views/stockage/ajaxFlatPop.php",
        method:"POST",
        async: false,
        data:{name:name},
        success:function(data)
        {
        fetch_data = data;
        },
        error:function()
        {
        alert("Problème");
        }
        });
        return fetch_data;
        };
        });

        ______________________________________
        ajaxFlatPop.php

        <?php
        if(isset($_POST["name"]))
        {
        $connect = mysqli_connect("localhost", "root", "", "mineraux");
        $output = '';
        $query = "SELECT * FROM stockage WHERE piece='".$_POST["name"]."'";
        $result = mysqli_query($connect, $query);
        while($row = mysqli_fetch_array($result))
        {
        $output = '
        Name : '.$row['nom'].'
        Type : '.$row['type'].'
        ';
        }
        echo $output;
        }
        ?>

        _____________________________________
        dans ma vue, qui est un svg, j’active le popover pour chaque path

        Ca fonctionne d’ailleurs déjà pour le titre.
        mon problème, c’est l’URL et éventuellement la gestion du csrf

        PS : je peux utiliser des balises dans mes commentaires ?

        1. Pour comprendre le contexte de cette question, j’ai tracé un plan svg de mon appartement (grâce au tuto carte de france !).
          Quand je survole les pièces, un tooltip apparaît en affichant le nom des pièces. Quand je clique dessus, je renseigne un input (pièce) avec la valeur (nom de la pièce).
          Dans chaque pièce, je stocke divers conteneurs (boites, cartons, armoire…)

          Je voudrais afficher dynamiquement l’inventaire de chaque pièce avec un popover. Le titre est le nom de la pièce (fonctionne déjà) et le contenu afficherait le nb de conteneurs dans cette pièce.
          Toutes ces informations se trouvent dans une table ‘stockage’ de ma base de données.

          1. Si, j’utilise une structure Laravel.
            Mon plan svg est dans une blade. Mes popovers sont activés dans mon fichier custom.js stocké dans le répertoire public.
            S’il existe une logique Laravel, je serai heureux que tu me l’expliques. c’est finalement l’objet de ma question. Le code ci-dessus n’est pas adapté à Laravel.

          2. Si tu utilises Laravel alors autant créer une route, passer par un contrôleur et gérer ça de façon classique.

            D’autre part si les données ne sont pas trop lourdes peut-être que ça vaudrait le coup de partir sur une SPA.

          3. Avec la méthode classique (si je comprends bien ce qu’est la méthode classique), je me retrouve avec un objet qui contient les éléments de ma requête.
            Il faudrait que j’injecte l’élément correspondant dans chaque balise path de mon svg. Ce qui suppose de faire un foreach sur tous les éléments path de mon svg et d’y ajouter un test if pour que l’élément de ma requête corresponde bien à mon path.
            Les paths ne peuvent pas être générés dynamiquement à moins de stocker le chemin d= »… » lui aussi dans la base de données.
            Il n’y a pas un moyen d’injecter le résultat directement dans le content (fichier custom.js) ?
            $(‘.popoverEl’).popover({
            trigger:’hover’,
            title: roomName,
            content: contentRoom,
            html: true
            });

          4. J’ai pas trop la vision d’ensemble mais il me semble qu’il faudrait placer tous les datas dans un objet et aller chercher dynamiquement les informations dans cet objet au survol.

          5. Si tu reprenais ton exemple d’appli avec la carte de France en svg et que dans ton tooltip, tu voudrais afficher le nom de la région (ce qui est déjà le cas) mais aussi le nombre d’habitants de cette région (nombre stocké dans une table dans un BD), comment t’y prendrais-tu ?

          6. Ça ne serait pas très judicieux d’être obligé d’aller chercher des informations sur le serveur quand on veut afficher un tooltip au survol, on va devoir attendre la réponse et ça va énerver l’utilisateur. Donc je me serais débrouillé pour disposer de toutes les informations sur le client auparavant.

          7. Pour l’instant, la seule façon que je connaisse en Laravel pour passer les données au client est par une requête dans le controller. Controller appelé par ma vue via une route. Mais dans mon cas, ma blade qui contient mon svg est incorporée dans deux vues différentes. Je ne vois donc pas d’action pour déclencher mon controller via une route, à moins de pouvoir le faire avec mon tooltip.

            Comme tu le sens sans doute, je débute. Je suis très content de ce que j’ai fait mais là, il y a des notions qui m’échappent.

          8. Étant donné que tu veux afficher des informations au survol il faut que tu ailles les chercher avant. Dans un scénario classique on charge la page pour que le visuel soit là et ensuite (par exemple dans un événement de chargement de la page) en Ajax on va chercher les informations complémentaires, quitte à faire apparaître un loader à l’utilisateur si ça doit un peu durer. Il faut que tu aies toutes tes données déjà dans une variable Javascript pour renseigner ton popover rapidement. Tu crées une route pour cette requête Ajax et et traitement dans un contrôleur.

  2. 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