Laravel 8

Livewire – introduction

Laravel 8, comme la plupart des versions, a apporté son lot de nouveautés : un remaniement des factories, un dossier pour les modèles, une sécurisation des accès (rate limiting), les espaces de noms dans le routage… On est bien habitués à tout ça et on s’adapte au fil des versions. Mais c’est sans doute au niveau de la gestion du frontend que les évolutions sont les plus marquantes. On a été encouragé pendant un bon moment à utiliser Vue.js et Bootstrap, on s’est retrouvés avec un package indépendant pour l’authentification (laravel/ui). Et puis voilà que maintenant débarque Jetstream avec son acolyte Fortify. J’ai déjà évoqué longuement ces nouveautés dans mon cours.

Mais ce que je n’ai pas développé ce sont les technologies utilisées : Tailwind et Livewire. Le premier est un framework CSS que je trouve personnellement assez verbeux mais qui connaît un certain succès. Le second est un ovni que je vous propose de commencer à découvrir dans cet article. Je dois avouer que ma première impression en abordant Livewire a été assez négatif, je me suis dit « voilà un outil pour ceux qui rament un peu avec Javascript ». Parce que la proposition est la suivante : plutôt que d’utiliser un framework Javascript comme Vue ou React pourquoi ne pas quitter Laravel et de tout coder en PHP ? la proposition a de quoi surprendre parce que le PHP est loin du navigateur et qu’il faut assurer la liaison entre les deux…

Installation

Livewire est un package pour Laravel qui s’installe classiquement avec composer :

composer require livewire/livewire

Mais quand on installe Laravel, si on veut déjà un système d’authentification complet, on installe Jetstream qui va lui-même installer Livewire comme dépendance (à moins qu’on choisisse Inertia) :

composer require laravel/jetstream
php artisan jetstream:install livewire
npm install
npm run dev
php artisan migrate

Donc avec Jetstream on dispose de Livewire et on va du coup s’en servir.

Créer un composant

Livewire fonctionne avec des composants. Pour en créer un c’est tout simple :

php artisan make:livewire ShowPosts

On se retrouve avec deux fichiers créés (et deux dossiers qui n’existaient pas auparavant) :

Le premier comporte intendance (la classe) et le second s’occupe de l’aspect (la vue).

Voyons le code généré dans la classe :

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class ShowPosts extends Component
{
    public function render()
    {
        return view('livewire.show-posts');
    }
}

On a une méthode render et on retourne la vue générée.

Dans la vue on a :

<div>
    {{-- Because she competes with no one, no one can compete with her. --}}
</div>

Donc pas grand chose pour le moment.

Utiliser un composant

Maintenant qu’on sait créer un composant voyons comment l’utiliser. Il y a deux façons de le faire :

  • dans une vue avec une nouvelle commande blade : @livewire(‘show-posts’)
  • dans une vue complète, dans ce cas le composant est la vue

C’est cette deuxième option qui est codée par défaut comme on l’a vu ci-dessus. Dans ce cas il nous faut aussi créer une route pour utiliser le composant :

use App\Http\Livewire\ShowPosts;

Route::get('posts', ShowPosts::class);

Si on fait ça avec une installation toute fraiche de Laravel on va tomber sur une erreur en utilisant l’url …/posts :

Le souci vient du fait que par défaut lorsqu’on utilise une page complète pour un composant il va automatiquement utiliser le layout resources/views/layouts/app.blade.php. Il y faut un emplacement {{ $slot }}. Or ce layout est pour les utilisateurs authentifiés, ça va donc fonctionner si on a une authentification active. Mais pour le moment on va plutôt spécifier le layout à utiliser :

public function render()
{
    return view('livewire.show-posts')->layout('layouts.guest');
}

Pour que ça fonctionne il faut deux choses dans le layout :

  • un emplacement {{ $slot }} dans lequel va se loger le composant
  • une insertion des scripts de Livewire pour les capacités réactives qu’on va voir plus loin avec @livewireScripts

Par défaut notre layout n’a pas le deuxième éléments, on va donc l’ajouter :

<body>
    <div class="font-sans text-gray-900 antialiased">
        {{ $slot }}
    </div>
    @livewireScripts
</body>

Maintenant on n’a plus d’erreur mais évidemment une page vide puisqu’on a rien dans notre vue. On va vérifier si ça fonctionne :

<div>
    <p>coucou</p>
</div>

Vous devriez avoir un simple coucou sur la page.

Les propriétés

Les composants de Livewire ont besoin de données. On peut créer des propriétés dans la classe :

class ShowPosts extends Component
{
    public $message = 'Coucou !';

Une propriété publique dans la classe est automatiquement disponible dans la vue :

<p>{{ $message }}</p>

Jusque là on n’a rien inventé de bien utile. Par contre ça va devenir plus intéressant avec la liaison de données. On connait bien ça avec Vue.js par exemple : on synchronise la valeur d’un élément sur la page web avec la propriété du composant.

On va recoder notre vue :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
      <input wire:model="message" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
      <p>{{ $message }}</p>
    </div>
  </div>
</div

On ne va pas s’attarder sur les classes de Tailwind, ce n’est pas le sujet (qui a dit que c’était verbeux ?). J’ai d’ailleurs juste repris la mise en forme utilisée dans les formulaire de l’authentification pour garder le visuel. Les éléments importants ici sont :

<div>
  ...
      <input wire:model="message" ... />
      <p>{{ $message }}</p>
  ...
</div

C’est l’attribut wire:model qui relie le champ de saisie à la propriété. Maintenant quand on change le texte dans le champ de saisie il s’actualise au-dessous :

Maintenant la question qu’on peut se poser c’est : comment ça fonctionne ?

On peut remarquer qu’à chaque changement on a une requête, il part une requête POST de la forme …/livewire/message/show-posts avec ce corps :

On a au retour une réponse JSON :

Sous le capot Livewire utilise Alpine pour gérer le Javascript sur la page.

Je ne sais pas ce que vous en pensez mais franchement c’est lourd ce système ! On peut faire la même chose avec quelques lignes de Javascript, mais ce n’est là que le principe de fonctionnement du système, et puis on peut améliorer les choses. Par défaut le rafraichissement s’effectue tous les 150 ms. On peut régler cette valeur comme on veut :

wire:model.debounce.1000ms="message"

Il est aussi possible de ne lancer l’actualisation qu’à la perte du focus :

wire:model.lazy="message"

On peut même reporter l’actualisation à la prochaine requête…

Les propriétés calculées

On peut aussi avoir des propriétés calculées (comme avec Vue.js), alors là évidemment ça devient plus intéressant parce qu’on a besoin d’information sur le serveur. Imaginons que nous avons deux utilisateurs dans notre table users (il vous suffit d’utiliser le formulaire d’enregistrement). Alors on peut créer cette propriété calculée (computed) :

use App\Models\User;
use Livewire\Component;

class ShowPosts extends Component
{
    public $index = 1;
    public function getUserProperty()
    {
        return User::find($this->index);
    }

Et dans la vue :

<input wire:model="index" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
<p>{{ $this->user->name }}</p>

Maintenant quand on entre l’index dans la zone de texte le nom de l’utilisateur correspondant apparaît au-dessous :

Là évidemment ça devient bien plus intéressant parce qu’on n’a pas à se soucier de toute l’intendance d’Ajax !

Les actions

Une action dans Livewire c’est la capacité à écouter une action sur la page web et d’appeler une méthode dans le composant pour modifier la page. c’est donc un pas de plus dans l’interactivité par rapport aux propriétés calculées vues ci-dessus.

Préparation

Pour montrer ça on va ajouter une colonne dans notre table users. On crée donc une migration :

php artisan make:migration alter_users_table --table=users

On complète le code de la migration :

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AlterUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->integer('note');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('note');
        });
    }
}

Et on lance la migration :

php artisan migrate

Maintenant on dispose d’une colonne note.

Action simple

On va coder ainsi notre composant :

<?php

namespace App\Http\Livewire;

use App\Models\User;
use Livewire\Component;

class ShowPosts extends Component
{
    public $note = 0;

    public function noter()
    {   
        $user = User::find(1);
        $user->note = $this->note;
        $user->save();
    }

    public function render()
    {
        return view('livewire.show-posts')->layout('layouts.guest');
    }
}

On a :

  • une propriété note
  • une action noter

Dans la vue :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
    <input wire:model.defer="note" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
    <button wire:click="noter" class="mt-4 bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
      Noter
    </button>
    </div>
  </div>
</div

On a :

  • une liaison de données avec la propriété note : wire:model.defer= »note » (on prevoit defer pour éviter les multiples requêtes)
  • un bouton pour noter avec une action sur le click : wire:click= »noter »

Maintenant quand on met une note dans la zone de texte et qu’on clique sur le bouton on retrouve la valeur de la note dans la colonne note de l’utilisateur (ici on a pris simplement le premier, mais on pourrait évidemment rendre ça dynamique ou prendre l’utilisateur connecté) :

Passage de paramètre

On peut passer un paramètre pour l’action. Poursuivons notre exemple en transmettant l’index de l’utilisateur . Dans la classe on ajoute une propriété $index et on utilise le paramètre dans l’action :

public $note = 0;
public $index = 1;

public function noter($indexPost)
{   
    $user = User::find($indexPost);
    $user->note = $this->note;
    $user->save();
}

Dans la vue on ajoute la saisie de l’index et la transmisison du paramètre :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
      <label class=block font-medium text-sm text-gray-700">
        Index de l'utilisateur
      </label>
      <input wire:model.defer="index" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
      <label class=block font-medium text-sm text-gray-700">
        Note
      </label>
      <input wire:model.defer="note" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
      <button wire:click="noter({{ $index }})" class="mt-4 bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
        Noter
      </button>
    </div>
  </div>
</div>

La partie intéressante est le passage du paramètre :

wire:click="noter({{ $index }})"

On a donc deux zones de texte :

On peut maintenant choisir l’utilisateur à noter. Bon c’est sommaire et pas du tout réaliste mais ça donne le principe de fonctionnement. On pourrait par exemple remplir une liste de choix avec les noms des utilisateurs. je ne vais pas le faire pour rester simplement dans les principes de fonctionnement.

La validation

Dans mon exemple précédent je n’ai prévu aucun contrôle quant aux valeurs transmises. dans une approche réaliste il nous faut évidemment une validation des valeurs. La bonne nouvelle c’est que le système est le même que pour la validation dans Laravel.

Validation classique

On va changer le code pour adopter quelque chose de plus réaliste avec un formulaire :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
      <x-jet-validation-errors class="mb-4" />
      <form wire:submit.prevent="submit">
        <label class=block font-medium text-sm text-gray-700">
          Index de l'utilisateur
        </label>
        <input wire:model.defer="index" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
        <label class=block font-medium text-sm text-gray-700">
          Note
        </label>
        <input wire:model.defer="note" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
        <button type="submit" class="mt-4 bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
          Noter
        </button>
      </form>
    </div>
  </div>
</div

Pour l’affichage de erreurs de validation j’utilise le composant de Jetstream x-jet-validation-errors.

Pour notre composant on a mainstenant ce code :

<?php

namespace App\Http\Livewire;

use App\Models\User;
use Livewire\Component;

class ShowPosts extends Component
{
    public $note = 0;
    public $index = 1;

    protected $rules = [
        'note' => 'required|integer|between:0,20',
        'index' => 'required|exists:users,id',
    ];

    public function submit()
    {   
        $this->validate();
        $user = User::find($this->index);
        $user->note = $this->note;
        $user->save();
    }

    public function render()
    {
        return view('livewire.show-posts')->layout('layouts.guest');
    }
}

Le nom du composant n’est plus trop appropié mais bon, on le garde.

On a donx deux propriétés :

  • note
  • index

On prévoie une validation des deux propriétés :

protected $rules = [
    'note' => 'required|integer|between:0,20',
    'index' => 'required|exists:users,id',
];

Et pour le reste c’est pratiquement pareil. Maintenant on a un contrôle de la validité des entrées :

Bon je n’ai pas traduit en français mais c’est une autre histoire…

Mais on peut personnaliser les messages selon l’erreur :

protected $messages = [
    'note.integer' => 'C\'est quand même mieux un nombre pour une note !',
];

Validation en temps réel

Livewire va plus loin en permettant une validation en temps réel lors de la saisie de la valeur sans attendre la soumission. Il suffit d’ajouter un hook :

public function updated($note)
{
    $this->validateOnly($note);
}

On choisi la propriété qu’on veut valider avec validateOnly (sinon on validerait toutes les valeurs).

Maintenant quand on entre une valeur pour la note on aura la validation instantanément sans attendre la soumission.

Imbrication de composants

Les composants de Livewire peuvent être imbriqués (nested), autrement dit on peut mettre des composants dans des composants. On va poursuivre notre exemple avec cette fois deux composants :

  • un composant parent qui va afficher un utilisateur
  • un composant enfant qui permet de noter l’utilisateur

On commence par créer les deux composants :

php artisan make:livewire ShowUser
php artisan make:livewire NoteUser

On va prévoir une route avec un paramètre pour préciser l’index de l’utilisateur :

use App\Http\Livewire\ShowUser;

Route::get('user/{user}', ShowUser::class);

Comme Livewire est parfaitement intégré à Laravel on peut utiliser la liaison de données (Route Model Binding), donc là je précise le nom de paramètre user pour que Laravel comprenne ce que je veux.

Avec Livewire on n’utilise pas de contrôleur alors comment récupérer le paramètre ? On utilise la méthode mount (vous pouvez trouver tous les hooks diponibles dans la documentation) :

<?php

namespace App\Http\Livewire;

use App\Models\User;
use Livewire\Component;

class ShowUser extends Component
{
    public $user;

    public function mount(User $user)
    {
        $this->user = $user;
    }

    public function render()
    {
        return view('livewire.show-user')->layout('layouts.guest');
    }
}

Là notre composant récupère bien le paramètre et affecte la propriété $user qui sera disponible dans la vue :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
        <p>Nom = {{ $user->name }}</p>
        <p>Email = {{ $user->email }}</p>
    </div>
  </div>
</div

Avec une url de la forme …/user/1 on obtient les renseignements sur l’utilisateur :

On va maintenant inclure le composant pour noter l’utilisateur. On code la classe NoteUser :

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class NoteUser extends Component
{
    public $note = 0;
    public $user;

    protected $rules = [
        'note' => 'required|integer|between:0,20',
    ];

    public function submit()
    {   
        $this->validate();
        $this->user->note = $this->note;
        $this->user->save();
    }

    public function render()
    {
        return view('livewire.note-user');
    }
}

Et la vue note-user :

<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
  <x-jet-validation-errors class="mb-4" />
  <form wire:submit.prevent="submit">
    <label class=block font-medium text-sm text-gray-700">
      Note
    </label>
    <input wire:model.defer="note" class="form-input rounded-md shadow-sm block mt-1 w-full" type="text"/>
    <button type="submit" class="mt-4 bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
      Noter
    </button>
  </form>
</div>

Il ne reste plus qu’à insérer ce composant dans l’autre, donc dans la vue show-user :

<div>
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    ...
    @livewire('note-user', ['user' => $user])
  </div>
</div

On prend la précaution d’envoyer l’information de l’utilisateur dans le composant enfant.

Et ça devrait fonctionner avec la validation !

Conclusion

J’avais un fort a priori contre Livewire mais je dois reconnaitre qu’il est intéressant. Je n’ai fait dans cet article que montrer les fonctionnalités de base du système proposé. Je conseille vivement de consulter la documentation et surtout les vidéos qui sont très bien faites. C’est une autre façon d’envisager la gestion du frontend en allégeant le codage de ce côté. On fait de l’Ajax sans s’en rendre compte. Il faut évidemment bien mesurer la charge à assumer côté serveur parce que ça peut vite devenir lourd. D’autre part étant donné le délai qui peut se produite en attendant une réponse on peut se retrouver avec une réactivité dans les chaussettes. Pour ce cas Livewire prévoit une gestion de l’attente.

Livewire connaît un certain succès qui me semble légitime. Est-ce que ce sera une lame de fond qui va nous emporter ou juste une mode passagère ? On verra bien. En attendant Notre framework préféré l’a adopté pour l’authentification alors autant l’utiliser…

 

 

Print Friendly, PDF & Email

2 commentaires

  • fabBlab

    Bonjour,

    J’aime beaucoup le principe de Livewire, sauf les classes de Tailwind 🙁
    Mais on doit pouvoir l’utiliser sans.
    Ne pas avoir à écrire des requêtes assez basiques et répétitives peut être bien sympa.
    Je suis d’accord qu’il faut limiter les appels au serveur, mais c’est vrai aussi pour le JS « cousu main ».
    Le « defer » peut aider à cela avec Livewire.

    Et sinon quelques bricoles à rectifier dans l’article, comme à mon habitude 🙂 :

    Dans l’exemple donné pour la liaison des données avec un input :

    use App\Models\User;
    use Livewire\Component;
    class ShowPosts extends Component
    {
    public $index = 1;
    public function getUserProperty()
    {
    return User::find(1);
    }

    La méthode ne peut que retourner tout le temps le même utilisateur.
    Cela fonctionne en remplaçant par :

    public function getUserProperty()
    {
    return User::find($this->index);
    }

    Pour l’exemple montrant comment passer un paramètre à la méthode appelée par la vue :

    public $note = 0;
    public $index = 1;
    public function noter($index)
    {
    $user = User::find($index);
    $user->note = $this->note;
    $user->save();
    }

    Pour que cela fonctionne, j’ai dû renommer le paramètre pour éviter la confusion avec l’attribut ayant le même nom :

    public function noter($indexPost)
    {
    $user = User::find($indexPost);
    $user->note = $this->note;
    $user->save();
    }
    Et enlever le « defer » du champ permettant de saisir l’index, car sinon la fonction était appelée avant que la valeur du paramètre change dans la vue.
    Le « defer » ne pose par contre pas de problème avec le formulaire suivant qui a un bouton submit.

    Pour les tests en cours de saisie :
    public function updated($note)
    {
    $this->validateOnly($note);
    }
    De nouveau il faut enlever les « defer » des champs que l’on souhaite valider lors de la saisie et cela teste le champ modifié quel-qu’il soit : https://laravel-livewire.com/docs/2.x/input-validation#real-time-validation

    Sinon un détail, mais dans plusieurs vues, le «  » final est mal fermé.

    Voilà pour mon rapport 🙂
    Encore merci pour ces tutos !

Laisser un commentaire