Du détail en modal

Je ne sais pas pourquoi mais c’est un sujet qui revient fréquemment, celui de pouvoir utiliser Ajax dans Laravel pour renseigner une page modale. Il y a eu sur le sujet encore une question sur ce blog. Plutôt que de répondre de façon fractionnée je préfère dédier un article sur le sujet.

On va partir sur une installation neuve de Laravel 7 et imaginer un scénario avec des utilisateurs qui font des commandes (relation 1:n) et pour ces commandes on va avoir des produits (encore une relation 1:n). C’est quelque chose de classique dans le cadre d’une application de commerce en ligne. Je suis d’ailleurs en train d’en développer une pour mes besoins personnels et je dois dire que c’est très intéressant et instructif, il est fort possible que je dédie une série d’articles sur le sujet.

Installation de Laravel

On commence donc pas créer une application Laravel :

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

Au moment où j’écris cet article Laravel en est à la version 7.6.

On ne va pas mettre en place d’authentification pour simplifier.

On crée une base de données avec le même nom modaldetail. On renseigne le fichier .env pour accéder à cette base :

DB_DATABASE=modaldetail
DB_USERNAME=root
DB_PASSWORD=

On va ajouter la package pour le frontend :

composer require laravel/ui

Et générer les élément de base avec Bootstrap :

php artisan ui bootstrap

Les données

On a par défaut un modèle et une migration pour les utilisateurs. On va créer ça aussi pour les commandes et les produits :

php artisan make:mode Order -m
php artisan make:mode Product -m

On se retrouve avec ces migrations :

Pour les commandes on va ajouter deux champs et la clé étrangère :

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
        $table->string('reference', 8);
        $table->decimal('total');
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
    });
}

Et pour les produits 3 champs et la clé étrangère :

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
        $table->string('name');
        $table->decimal('price');
        $table->integer('quantity')->defaut(0);
        $table->foreignId('order_id')->constrained()->onDelete('cascade');
    });
}

Dans les modèles on va compléter pour l’assignement de masse et les relations, dans User :

public function orders()
{
    return $this->hasMany(Order::class);
}

Dans Order :

protected $fillable = [
    'user_id', 'reference', 'total',
];

public function products()
{
    return $this->hasMany(Product::class);
}

public function user()
{
    return $this->belongsTo(User::class);
}

Pour avoir des données on va créer un factory pour les commandes :

php artisan make:factory OrderFactory --model=Order

On va compléter le code :

...
use Illuminate\Support\Str;

$factory->define(Order::class, function (Faker $faker) {
    return [
        'reference' => strtoupper(Str::random(8)),
        'total' => mt_rand (1000, 5000) / 100,
        'user_id' => mt_rand(1, 5),
    ];
});

On crée aussi un factory pour les produits :

php artisan make:factory ProductFactory --model=Product

Avec ce code :

$factory->define(Product::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
        'price' => mt_rand (100, 500) / 100,
        'quantity' => mt_rand(1, 3),
    ];
});

Et enfin on code la classe DatabaseSeeder :

<?php

use Illuminate\Database\Seeder;
use App\ { User, Order, Product };
use Illuminate\Support\Str;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        factory(User::class, 5)->create();

        factory(Order::class, 30)
        ->create()
        ->each(function ($order) {
            $order->products()->createMany(
                factory(Product::class, rand(3, 8))->make()->toArray()
            );
      });
    }
}

Il ne reste plus qu’à lancer :

php artisan migrate --seed
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.49 seconds)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (0.3 seconds)
Migrating: 2020_04_19_103609_create_orders_table
Migrated:  2020_04_19_103609_create_orders_table (0.92 seconds)
Migrating: 2020_04_19_103647_create_products_table
Migrated:  2020_04_19_103647_create_products_table (0.95 seconds)
Database seeding completed successfully.

Routes et contrôleur

On crée un contrôleur :

php artisan make:controller OrderController

On va prévoir deux méthodes :

  • pour afficher la liste des commandes d’un utilisateur
  • pour afficher le détail d’une commande
<?php

namespace App\Http\Controllers;

use App\{ User, Order };

class OrderController extends Controller
{
    public function index($id)
    {
        $user = User::with('orders')->findOrFail($id);

        return view('orders', compact('user'));
    }

    public function detail($id)
    {
        $order = Order::with('products')->findOrFail($id);

        return view('detail', compact('order'));
    }
}

Et les deux routes pour y mener :

Route::name('orders')->get('commandes/{id}', 'OrderController@index');
Route::name('detail')->get('commandes/{id}/detail', 'OrderController@detail');

Je ne gère aucune sécurité pour cet exemple. On pourrait d’ailleurs gérer les détails en simple API.

Les vues

On crée le template (layout) :

Avec ce code :

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ config('app.name', 'Laravel') }}</title>
    <script src="{{ asset('js/app.js') }}" defer></script>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <main class="container">
        @yield('content')
    </main>
    @stack('scripts')
</body>
</html>

Et la vue order :

Avec ce code :

@extends('layout')

@section('content')
  <br>
  <div class="card"">
    <div class="card-body">
      <h5 class="card-title">Liste des commande de {{ $user->name }}</h5>
      <table class="table table-striped table-hover">
        <thead>
          <tr>
            <th>#</th>
            <th>Référence</th>
            <th>Total</th>
            <th>date</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          @foreach($user->orders as $order)
            <tr>
              <th>{{ $order->id }}</th>
              <td>{{ $order->reference }}</td>
              <td>{{ number_format($order->total, 2, ',', ' ') . ' €' }}</td>
              <td>{{ $order->created_at->format('d/m/Y') }}</td>
              <td>{!! '<a href="' . route('detail', $order->id) . '" class="btn btn-xs btn-info btn-block">Voir</a>' !!}</td>
            </tr>
          @endforeach
        </tbody>
      </table>
    </div>
  </div>

@endsection

Maintenant avec un url du genre …/commandes/1 on obtient la liste des commandes de l’utilisateur :

Il ne nous reste plus qu’à ouvrir une page modale pour les détails d’un commande quand on clique sur le bouton Voir.

Détail de la commande

Je vais utiliser JQuery pour cet exemple mais évidemment ça pourrait être traité différemment.

Voici le code modifié de la vue orders :

@extends('layout')

@section('content')
  <br>
  <div class="card">
    <div class="card-body">
      <h5 class="card-title">Liste des commandes de {{ $user->name }}</h5>
      <table class="table table-striped table-hover">
        <thead>
          <tr>
            <th>#</th>
            <th>Référence</th>
            <th>Total</th>
            <th>date</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          @foreach($user->orders as $order)
            <tr>
              <th>{{ $order->id }}</th>
              <td>{{ $order->reference }}</td>
              <td>{{ number_format($order->total, 2, ',', ' ') . ' €' }}</td>
              <td>{{ $order->created_at->format('d/m/Y') }}</td>
              <td>{!! '<a href="' . route('detail', $order->id) . '" class="btn btn-xs btn-info btn-block">Voir</a>' !!}</td>
            </tr>
          @endforeach
        </tbody>
      </table>
    </div>
  </div>

  <div class="modal fade" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
      <div id="detail" class="modal-content">

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

@endsection

@push('scripts')

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

  $(() => {
    $('a').click(e => {
      let that = e.currentTarget;
      e.preventDefault();
      $.ajax({
        method: $(that).attr('method'),
        url: $(that).attr('href'),
        data: $(that).serialize()
      })
      .done((data) => {
        $('#detail').html(data);
        $('.modal').modal('show');
      })
      .fail((data) => {
        console.log(data);
      });
    });
  });

</script>

@endpush

J’ai ajouté la structure de base de la page modale et le Javascript pour que ça fonctionne.

On crée la vue pour le détail :

Avec le code pour remplir la page modale :

<div class="modal-header">
  <h5 class="modal-title">Détail de la commande {{ $order->reference }}</h5>
  <button type="button" class="close" data-dismiss="modal" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
<div class="modal-body">
  <table class="table table-striped table-hover">
    <thead>
      <tr>
        <th>#</th>
        <th>Nom</th>
        <th>Prix</th>
        <th>Quantité</th>
      </tr>
    </thead>
    <tbody>
      @foreach($order->products as $product)
        <tr>
          <th>{{ $product->id }}</th>
          <td>{{ $product->name }}</td>
          <td>{{ number_format($product->price, 2, ',', ' ') . ' €' }}</td>
          <td>{{ $product->quantity }}</td>
        </tr>
      @endforeach
    </tbody>
  </table>
</div>

Maintenant quand on clique sur un bouton on a l’ouverture de la page modale avec les produits :

Conclusion

On pourrait améliorer le code avec par exemple une icône animée pour l’attente de la réponse du serveur mais l’essentiel est là.

Print Friendly, PDF & Email

3 commentaires sur “Du détail en modal

  1. Merci pour l’article, je me suis abonné au rss 🙂

    Une super librairie pour implanter ça et milles autres choses, mais qui n’est pas très connue malheureusement, c’est unpoly : https://unpoly.com/

    Pas de JS, juste mettre up-modal= »adresse_de_la_page » et c’est bon !

    Présentation ici : https://unpoly.com/

    Je l’utilise depuis un certain temps pour Agorakit, un outil d’organisation pour collectifs basé sur laravel (et open source) : https://agorakit.org
    Franchement ça me fait gagner un temps fou.

Laisser un commentaire