
La TALL Stack
La Tall Stack est un preset pour Laravel qui combine plusieurs outils : Tailwind, AlpineJS, Laravel et Livewire. J’ai déjà parlé de ces outils dans ce blog. J’ai rédigé une introduction à Livewire et j’ai aussi parlé de Tailwind avec un petit comparatif. Je n’en ai pas forcément dit que du bien mais on ne peut nier la popularité de ces outils aussi j’ai jugé utile de revenir un peu dessus.
Tailwind comporte des classes qui permettent d’écrire du CSS sans en écrire. En gros la plupart des possibilités du CSS ont été compilés en nombreuses classes. Le résultat est qu’on peut se passer de feuilles de style, l’inconvénient c’est qu’on se retrouve avec un empilement de classes.
Alpine est une librairie Javascript très légère, déclarative, qui permet de mettre en place facilement des éléments interactifs. C’est un peu l’équivalent de Tailwind mais pour le Javascript. Elle a été créée par le concepteur de Livewire.
Livewire est un outil dédié à Laravel qui a pour objectif de créer de façon simple et rapide des composants interactifs sans se soucier de la gestion d’Ajax. En gros il gère le DOM et le met à jour selon la valeur de données qui sont sur le serveur et cela de façon automatique. Pour ceux qui galèrent avec Javascript et pour qui Ajax est une corvée, c’est évidemment une solution efficace !
Dans cet article je vais considérer un cas simple qu’on rencontre souvent. On a une liste de choix et selon ce qu’on sélectionne, on remplit une seconde liste avec les valeurs correspondant à ce choix. Pour l’exemple je vais utiliser des catégories de livres et des livres associés. On va traiter ça dans un premier temps de façon classique pour Ajax, puis avec Livewire. Dans les deux cas je vais utiliser Tailwind pour le CSS et me consacrer essentiellement à l’apport de Livewire.
Vous pouvez télécharger le code final de cet article ici.
Installation
Laravel
On crée une nouvelle installation de Laravel :
composer create-project laravel/laravel tallstack
On crée aussi une base de données et on renseigne le fichier .env :
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=tallstack DB_USERNAME=root DB_PASSWORD=
Preset
Ensuite on installe le preset :
composer require livewire/livewire laravel-frontend-presets/tall
On installe avec l’authentification :
php artisan ui tall --auth
Et on lance la génération pour le frontend :
npm install npm run dev
On arrive sur cette page d’accueil :
Et on a les formulaires pour l’authentification :
On en profitera pour utiliser le même layout.
Les données
Pour notre exemple on va avoir besoin de données. On a des catégories et des livres. On crée les modèles et les migrations :
php artisan make:model Category -m php artisan make:model Book -m
On ajoute la relation :
class Category extends Model { ... public function books() { return $this->hasMany(Book::class); } }
Pour la migration des catégories on se contente d’un nom :
public function up() { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name'); }); }
Et pour les livres également un nom et la clé étrangère :
public function up() { Schema::create('books', function (Blueprint $table) { $table->id(); $table->string('name'); $table->foreignId('category_id')->constrained(); }); }
Dans le seeder on prévoit quelques catégories et des livres associés :
<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { DB::table('categories')->insert([ ['name' => 'Humour'], ['name' => 'Littérature'], ['name' => 'Scolaire'], ['name' => 'Guides pratiques'], ['name' => 'Sciences sociales'], ['name' => 'Cuisine'], ]); DB::table('books')->insert([ ['name' => 'Brèves de comptoir', "Category_id" => 1], ['name' => 'La Castafiore', "Category_id" => 1], ['name' => 'Prises de bec', "Category_id" => 1], ['name' => 'Les grosses têtes', "Category_id" => 1], ['name' => 'Les meilleures blagues', "Category_id" => 1], ['name' => 'Les murmures du lac', "Category_id" => 2], ['name' => 'Rien de sérieux', "Category_id" => 2], ['name' => 'Massada', "Category_id" => 2], ['name' => 'La montée des eaux', "Category_id" => 2], ['name' => 'Mon cahier d\'écriture', "Category_id" => 3], ['name' => 'Parcoursup', "Category_id" => 3], ['name' => 'Croc blanc', "Category_id" => 3], ['name' => 'Tropismes', "Category_id" => 3], ['name' => 'Halte au virus !', "Category_id" => 3], ['name' => 'Fiscalité immobilaire', "Category_id" => 4], ['name' => 'Créer une SCI', "Category_id" => 4], ['name' => 'La copropriété', "Category_id" => 4], ['name' => 'Le diable', "Category_id" => 5], ['name' => 'Penser librement', "Category_id" => 5], ['name' => 'L\'homme', "Category_id" => 5], ['name' => 'La religion', "Category_id" => 5], ['name' => 'Le suicide', "Category_id" => 5], ['name' => 'Le pain', "Category_id" => 6], ['name' => 'Les entrées', "Category_id" => 6], ['name' => 'Les brioches', "Category_id" => 6], ]); } }
C’est rustique mais efficace…
Il ne reste plus qu’à lancer migrations et population :
php artisan migrate --seed
On a tout ce qu’il nous faut pour nos essais !
Version classique
Dans un premier temps je vais traiter cette situation de façon classique avec du simple Javascript pour assurer l’interactivité du formulaire.
On crée un contrôleur :
php artisan make:controller BooksController --model=Category
Les routes :
use App\Http\Controllers\BooksController; Route::get('books', [BooksController::class, 'index']); Route::get('getbooks/{category}', [BooksController::class, 'getBooks']);
Il me faut en effet deux routes :
- une route pour afficher le formulaire au départ
- une route pour assurer l’actualisation de la liste des livres selon la catégorie sélectionnée
On crée la vue :
Pour le moment on ne prévoit pas l’interactivité et on utilise un layout déjà présent pour simplifier :
@extends('layouts.app') @section('title', 'Recherche documentaire') <div class="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8"> <div class="sm:mx-auto sm:w-full sm:max-w-md"> <h2 class="mt-6 text-3xl font-extrabold text-center text-gray-900 leading-9"> Recherche documentaire </h2> </div> <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div class="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10"> <form wire:submit.prevent="authenticate"> <div> <label for="category" class="block text-sm font-medium text-gray-700 leading-5"> Catégorie </label> <select id="categories" class="mt-1 rounded-md shadow-sm appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"> @foreach($categories as $id => $name) <option value="{{ $id }}">{{ $name }}</option> @endforeach </select> </div> <div class="mt-6"> <label for="password" class="block text-sm font-medium text-gray-700 leading-5"> Livres </label> <div id="books" class="mt-1 rounded-md shadow-sm appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"> </div> </div> </form> </div> </div> </div>
On complète le code du contrôleur :
public function index() { $categories = Category::pluck('name', 'id'); return view('books', compact('categories')); }
Dans un premier temps on se contente d’envoyer toutes les catégories.
Remarque : lorsqu’on a juste une ou deux colonnes à récupérer il est plus judicieux d’utiliser pluck parce que ça allège les requêtes à la base de données. Il y a un article récent qui synthétise toutes ces optimisations que j’ai pris un grand plaisir à consulter.
Maintenant avec l’url tallstack.ext/books on obtient le formulaire :
Évidemment pour le moment la liste des livres est vide.
On commence par préparer une vue pour générer cette liste :
@foreach($books as $book) <p>‣ {{ $book->name }}</p> @endforeach
On crée la méthode dans le contrôleur :
public function getBooks(Category $category) { $books = $category->books()->get(); return [ 'html' => view('bookslist', compact('books'))->render(), ]; }
Et il ne reste plus qu’à ajouter le Javascript dans la vue books :
<script> const headers = { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } const getBooks = async (id) => { const response = await fetch('/getbooks/' + id, { method: 'GET', headers: headers }); const data = await response.json(); if (response.ok) { document.getElementById('books').innerHTML = data.html } else { alert('Il y a une erreur !'); } } document.getElementById('categories').addEventListener('change', e => getBooks(e.target.value)); getBooks(document.getElementById('categories').value); </script>
On utilise fetch pour la requête Ajax. On initialise avec la première catégorie.
Maintenant ça fonctionne au chargement et quand on change de catégorie :
On voit que ce traitement très classique n’a rien de complexe mais qu’il oblige à créer pas mal de code.
Version Livewire
Avec Livewire on doit créer des composants, alors on en crée un :
php artisan make:livewire FindBooks
On se retrouve avec une classe :
Avec ce code par défaut :
<?php namespace App\Http\Livewire; use Livewire\Component; class FindBooks extends Component { public function render() { return view('livewire.find-books'); } }
On retourne une vue qui a aussi été créée :
Qui ne contient pas grand chose :
<div> {{-- Close your eyes. Count to one. That is how long forever feels. --}} </div>
Avec Livewire on a deux types de composant : en ligne ou pleine page.
Dans le premier cas on insère le composant dans une vue avec deux syntaxes possible dont l’instruction @livewire.
Dans le second cas le composant constitue une page complète et il faut donc prévoir une route, on va choisir cette possibilité et donc ajouter une route :
use App\Http\Livewire\FindBooks; Route::get('bookslivewire', FindBooks::class);
Dans un composant de Livewire on peut déclarer des propriétés et la magie tient au fait que ces propriétés deviennent automatiquement disponibles dans la vue. On a besoin de 3 propriétés :
- $catagories : pour la liste des catégories (identifiant et nom)
- $category_id : l’identifiant de la catégorie sélectionnée
- $books : la liste des livres de la catégorie sélectionnée
class FindBooks extends Component { public $categories; public $category_id = 1; public $books;
Il nous faut aussi des méthodes pour la mise à jour de ces propriétés. Déjà à l’initialisation on doit renseigner les catégories et les livres de la première catégorie. d’autre part on doit prévoir l’actualisation au changement de catégorie :
public function mount() { $this->categories = Category::pluck('name', 'id'); $this->books(); } public function books() { $this->books = Category::find($this->category_id)->books()->pluck('name'); }
On code ensuite la vue :
@section('title', 'Recherche documentaire') <div class="flex flex-col justify-center min-h-screen py-12 bg-gray-50 sm:px-6 lg:px-8"> <div class="sm:mx-auto sm:w-full sm:max-w-md"> <h2 class="mt-6 text-3xl font-extrabold text-center text-gray-900 leading-9"> Recherche documentaire </h2> </div> <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div class="px-4 py-8 bg-white shadow sm:rounded-lg sm:px-10"> <form wire:submit.prevent="authenticate"> <div> <label for="category" class="block text-sm font-medium text-gray-700 leading-5"> Catégorie </label> <select class="mt-1 rounded-md shadow-sm appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5" wire:model="category_id" wire:change="books"> @foreach($categories as $id => $name) <option value="{{ $id }}">{{ $name }}</option> @endforeach </select> </div> <div class="mt-6"> <label for="password" class="block text-sm font-medium text-gray-700 leading-5"> Livres </label> <div class="mt-1 rounded-md shadow-sm appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"> @foreach($books as $name) <p>‣ {{ $name }}</p> @endforeach </div> </div> </form> </div> </div> </div>
Pour le fonctionnement on prévoit d’appeler la méthode books quand on a un changement dans la liste :
wire:change="books"
Lavewire utilise des actions pour répondre à des changements sur la page.
Mais il faut aussi actualiser l’identifiant de la catégorie :
wire:model="category_id"
On fait là du data-binding, c’est à dire qu’on synchronise la valeur d’un élément de la page HTML, ici la valeur de la liste déroulante avec une propriété de Livewire, ici category_id.
Et c’est tout !
Maintenant ça fonctionne exactement de la même manière qu’on a vue précédemment.
Conclusion
Que nous apporte ce petit exemple comparatif ? On se rend compte que le codage avec Livewire est plus léger et rapide et on n’a pas du tout besoin de se préoccuper d’Ajax qui est entièrement pris en charge de façon automatisée. On crée des propriétés dans une classe et on les relie à des éléments de la page en les synchronisant, d’autre part on peut répondre à des actions de l’utilisateur.
Mais à la lecture des forums je me rends compte qu’il y a quand même rapidement des difficultés dès qu’on sort des cas simples. Comme je ne l’ai pas utilisé moi-même pour un projet conséquent j’ai du mal à me faire une opinion définitive.

