Ou Ziggy route dans une cellule Datatable PrimeVue en mode composant de cellule.
Je ne vais pas répéter ici ce qui a été vu précédemment, ce que l’on cherche à faire est un composant utile et générique pour pouvoir rediriger l’utilisateur vers une route nommée avec les paramètres adéquats (variables donc) avec un libellé pouvant être basé sur les données de la lignes (rowData).
La route
Ziggy va alimenter notre objet route. Ce dernier contiendra la définition de notre router Laravel (pour rappel quand même). On a une route nommée : maroute-edit = /maroute/{id} .
Le composant de lien de cellule
On a besoin de lui passer l’url et le texte à afficher, mais on aimerait profiter du rowData pour construire ce texte et cette url (du fait des paramètres à passer), il nous faut donc un moyen d’intervention du côté de l’appelant.
Pour le texte on peut imaginer que le paramètre ne soit pas le libellé directement mais une fonction callback recevant rowData en params, nous permettant de retourner le string que l’on aura produit potentiellement avec.
Pour l’url c’est plus compliqué, on sait juste que l’on veut travailler avec des routes nommées, donc une propriété qui recevra le nom de la route, mais quid des paramètres ? Dans mon cas j’ai eu un besoin de l’attribut de route ‘id’, mais dans mon rowData c’est l’attribut xyz_id qui matchait et non l’id de ma row, du coup il nous faudrait un mapper qui serait un tableau de clefs avec la valeur à prendre dans le rowData.
En gros l’astuce sera dans props où l’on passera le nom de la route, le mapping de paramètres et la fonction callback pour rédiger le contenu. Ainsi on aura dans mon cas ce rendu avec lien fonctionnel.
ScrollPanel est un composant PrimeVue (PrimeFaces) qui veut vous aider à faire défiler vos contenus dans un espace définit.
La démo est sympa, ça donne envie. Mais ! D’une part, c’est fait en Javascript, positionné par calcul, sans proposer de moyen de définir le positionnement (pensez à un t’chat); d’une autre, les bar peuvent sortir de leur piste en débordement (cas du DataTable redécoré avec ScrollPanel), sans compter que sans survol de la zone, les bar ne s’initialisent pas et induisent l’utilisateur en erreur. Trop d’erreurs pour le garder sur un projet, ni même comprendre l’intérêt hors de leur usage type en démo.
Les tinybar, c’est pas nouveau, depuis que Webkit nous a donné la main sur le style on s’en donne à coeur joie, même si certains navigateurs d’époque ne voulaient pas suivre, ou comme Firefox, continuent de faire bande à part (Firefox propose une autre formule plus minimaliste). Du coup, si ScrollPanel ne veut pas faire correctement un job qui existe déjà, ni apporter un plus au fait de créer un composant qu’une simple classe CSS peut résoudre, on peut faire le nôtre.
<div class="scrollpanel">
...
</div>
Comme je le disais, une simple classe CSS. Ceci dit, n’oubliez pas qu’il faut restreindre votre contenu, donc vous définirez une hauteur et/ou largeur, même si notre composant aura des valeurs par défaut.
Disséquons notre CSS. Déjà c’est écrit en SCSS car c’est plus cool 🙂 lisible, maintenable et organisé. Ensuite, j’ai utilisé des variables pour les couleurs comme on en trouve dans la plupart des librairies aujourd’hui, mais mettez ce que vous voulez : code hexadécimale, nom de couleur, rgb, … Enfin, il y a 2 solutions : on commence avec le préfixe scrollbar et on continue ensuite avec le préfixe -webkit.
scrollbar c’est la version Firefox de la solution, en mode minimaliste sans avoir toute la main sur ce que l’on veut avoir. Mais au moins on s’en approche et c’est toujours mieux que les scrollbar d’origine.
-webkit c’est la version webkit… bah oui, c’est comme le Port-Salut… Et là on profite de belles possibilités car les sélecteurs proposés nous laissent la main sur le CSS applicable sur la cible. ´Évidemment c’est non-standard, et en même temps c’est le moteur le plus répandu… allez comprendre. Du coup faites attention et n’hésitez pas à consulter caniuse ou MDN.
Au final vous aurez un système de scrollpanel, compatible et sans les erreurs de la version PrimeVue. Ceci dit, gardons un oeil dessus car PrimeFaces a un bon rythme de livraison et ils glissent souvent des correctifs intéressant dans leur livraisons mineures (ex: un fix virtual-scroll pour DataTable récemment), et ici, en préparant cet article, j’ai vu qu’ils ont ajouté une nouvelle mécanique pour permettre l’accès au style du DOM de leur composant avec une définition en paramètre (:pt pour Pass Through). Personnellement je préfère séparer mon CSS/SCSS (tel que leur thème et/ou une surcouche), surtout dans un esprit générique, car le passer en paramètre à chacun de nos usages générera une répétition de code, et ce n’est pas une bonne pratique (oui on peut faire un composant personnalisé qui englobe notre définition). Mais encore, le composant, bien qu’il évolue, ne propose toujours pas de fonctionnalité qui justifie son existence et cette façon de l’utiliser.
PrimeVue, c’est chouette mais c’est fait avec les pieds :/ Plus on explore un truc, plus on voit ce qui se cache derrière une belle apparence et un beau lot de composants. Personne n’est parfait et l’amélioration est continue, mais si vous regardez comment c’est codé, il y a dû y avoir quelques stagiaires au niveau architectural : des ponts de composants pas fait avec un réel impacte derrière (filtre de tableau (regardez la comparaison de date…) ou combinaison de solution des input), ou encore ici le scroll du tableau (datatable encore lui) qui ne peut pas être designé naturellement comme scrollpanel).
Qui dit problème, dit recherche de solution : mettons un scrollpanel autour d’un datatable, ça sera joli.
<ScrollPanel class="w-100">
<DataTable ...
Premièrement il faudra dire au DataTable d’être scrollable et avec une hauteur en flex, tout en le contredisant avec une belle injonction css. Et de l’autre, comme attendu, préciser au ScrollPanel sa hauteur.
.p-datatable.p-datatable-scrollable .p-datatable-wrapper { overflow: inherit }
.table-normal-height {
height: var(--content-height); /* La hauteur qu'il vous faut */
}
Outre le fait que ça fonctionne, vous aurez un effet de bord, dû au fait du code CSS ci-dessous qu’ils ont appliqué. Vous n’aurez pas la fin de la dernière ligne du tableau et vous pourrez danser sur votre tête, rien n’y fera.
Mais d’où vient ce 18px arbitraire et à quoi bon. Dans notre cas d’un tableau en flex : aucun. J’imagine donc qu’ils ont eu un cas nécessitant ce bricolage, mais nous ça nous emmerde. Du coup, on va reset localement.
En gros, en premier on définit la largeur de la zone de scroll et la couleur de fond. Ensuite, la couleur de la barre de défilement avec une transition pour le hover. Et donc enfin l’effet de survol (hover) avec une couleur de réaction plus vive.
Ceci fait suite à l’article Goo 3 avec Laravel 9, Sail, Inertia, Vue3, tailwindcss, Vite et PrimeVue, car on a oublié un morceau : le linter; analyseur syntaxique aidant à améliorer la qualité de son développement en respectant certains standards et bonnes pratiques. Cet oubli est dû on montage manuel de la solution, alors qu’en passant par la ligne de commande il est proposé parmi une suite d’options.
Mise en place
Pour vue, voici directement le linter et son plugin, en suivant leur guide, s’en suit le fichier de configuration que l’on peut personnaliser pour coller à ses besoins.
npm install --save-dev eslint eslint-plugin-vue
module.exports = {
extends: [
// add more generic rulesets here, such as:
// 'eslint:recommended',
'plugin:vue/vue3-recommended',
// 'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
],
rules: {
// override/add rules settings here, such as:
// 'vue/no-unused-vars': 'error'
}
}
Ça c’est la doc, mais évidemment ça ne se passera pas forcément bien car on est pas issu d’un projet généré par la ligne de commande, ben oui. J’ai donc cherché à comprendre un peu pourquoi le linter ne comprend pas Typescript, ni même module de son propre fichier de configuration. Ça commence mal.
Du coup, j’ai trouvé le npm @vue/eslint-config-typescript qui nous aide pas mal. J’ai installé et créé le fichier .eslintrc.cjs et j’y ai mis ce qu’ils indiquent :
Utilisant InteliJ, il vous faudra surement activer l’usage de la config.
Aménagements pour respecter le linter
C’est parti pour modifier pas mal de code, je vais donc corriger les blocs de code vu dans l’article PrimeVue : rendu et composant de cellule et vous expliquer la raison si elle n’est pas évidente.
Je commence par les modèles du tableau, component et son type Object devient any, du fait que <component> peut accepter un string en paramètre de :is.
Notez également la modification au niveau du prototype de renderer en transformant Object par un type plus précis, un tuple permettant de décrire la structure {clef: {}, clef2: {}, ...}, vous retrouverez cette modification plus loin également.
Ensuite, coté composant simple-table, différents points se présentent. D’abord les v-for sur composant inconnu ou <component> aiment avoir une clef d’identification unique (v-bind:key).
...
<Column v-for="(c, ci) in cols" v-bind:key="ci" :field="c.field" :header="c.header" :sortable="c.sortable">
<template #body="colProps" v-if="c.components">
<component v-for="(comp, i) in c.components"
v-bind:key="i"
...
Le linter râlera également sur certaines valeurs par défaut des props ou du type de data. default est une fonction et data de rowClass trouve une définition any dans leur code et rien ailleurs.
map râlait déjà que son hôte ne soit pas obligatoirement défini, ce qui est faux car required dans les props, j’imagine qu’il ne fait pas le liens, Angular a ce genre de soucis aussi. Et pour faire propre vu que l’on rend un objet neuf au retour de map, on le type.
Nous revoilà avec notre tuple, cette fois adapté pour les events. Et vu qu’on pourrait ne pas en donner, mais qu’on l’utilise, il vaut mieux avoir un contrôle et un retour propre. Attention que si on avait utilisé l’écriture events?: il aurait râlé car les paramètres facultatifs doivent se trouver après les impératifs. Et pour bien faire on type ev en Object.
Conclusion
Comme vous l’aurez peut-être expérimenté ou lu ici, si ce n’est pas fait à votre place, ce n’est pas forcément évident à mettre en route et à définir.
Il y a moyen de perdre beaucoup de temps si on ne s’y prend pas correctement dès le départ, c’est un réflexe à avoir. Et ici ce ne sont que quelques broutilles, ça fonctionne MAIS ce n’était pas assez bien. Et même si c’est peu de chose, cela aura pris à un padawan testeur une bonne après-midi.
Pensez à installer Vue.js devtools sur votre navigateur, ça vous sera utile pour voir que les données sont bien là, ou non.
Nous allons donc utiliser le DataTable de PrimeVue tel que nous l’avons fait dans le premier article et en faire un composant dynamique générique. Et malheureusement ce n’est pas un copier-coller de la version Angular, ça serait trop simple ;). Notez que je ne réexpliquerai pas tout vu que le détail se trouve dans les articles cités.
Contexte de base
On a donc un composant vue qui contient le DataTable, et une page qui appellera ce composant. Les données seront fournie automatiquement par Inertia, nous nous concentrerons sur le typage Typescript, la structure du composant et le raisonnement.
Composant du tableau
Commençons par notre nouveau composant simple-table. Il s’agit donc de configurer un DataTable basique et de dynamiser les colonnes, pour cela on utilise une boucle v-for sur un composant Column en lui passant les paramètres qui nous intéressent. Ensuite, on peut se pencher sur le rendu de la valeur de la cellule via le template #body.
Pour rappel nous sommes en mode composition et en TypeScript. Il nous faut maintenant définir les propriétés de notre composant, tel que data pour les données, colsDef pour la définition des colonnes, defaultSort pour la colonne à trier par défaut et rowClass mentionné juste avant. Notez qu’on en profite pour typer fortement les données que l’on requiert.
L’idée reste d’envoyer un minimum de configuration et d’en déduire les valeurs par défaut, tel que la méthode de rendu de cellule par défaut ou le fait de pouvoir trier ou non une colonne. Pour ceci on passera par une variable computed , sans oublier de la typer, pour générer la liste des configurations de colonnes utilisées par DataTable.
En parlant de typage, voici l’interface utilisée pour la définition de colonnes :
Côté code j’ai préparé de fausses données que j’ai simplifié pour l’exemple, ainsi que les méthodes utilitaires ou de rendu. Nous sommes dans un contexte de logiciel comptable et on affiche ici des factures, on aura 4 colonnes qui auront besoin d’afficher une valeur présentable, tel que la date ou le statut.
Notez que par rapport à la version Angular j’ai modifié l’interface CellComponent pour coller à la suite et à l’univers Vue. Il nous faut maintenant modifier notre composant simple-table.
On conditionnera l’usage au niveau template avec l’option components définie, sinon le rendu de cellule fera son office. Notez la simplicité, et donc la complexité/contrainte, de ce composant <component>. On doit lui envoyer un composant et ses propriétés (ainsi que son modèle si on regarde la définition totale). Là normalement vous raccrochez le wagon de l’interface, il nous reste donc l’application de tout ceci.
On ajoute une colonne d’actions à notre définition et on souhaite un bouton icône, là j’utilise simplement un Button de PrimeVue tel quel et je précise en props l’icône désirée et le style du bouton via sa classe, c’est pas plus sorcier au final. Il reste le markRaw qui est la réponse à une erreur qui se produit sans ^^, à priori la belle manière, je n’ai pas creusé ce coup-ci.
Passer les valeurs de la ligne
Notre composant aimera surement savoir son contexte : la ligne courante du tableau et la colonne dans laquelle il se trouve, ce sont des bons repères pour agir ;). On commencera par étendre le v-bind avec le spread opérator, mais ensuite pour les events c’est plus compliqué, nous allons les intercepter.
On redéfinit les définitions d’appel des événements pour ajouter 2 paramètres, la ligne et son champs courant. En ayant préalablement vérifié sa structure. Et du coup l’usage change ainsi :
Si comme moi vous avez décider de mélanger des trucs et que la documentation vous manque dans ce cas particulier : bienvenue !
Ici la problématique est que le système de menu de Prime utilise un router-link, mais que nous, avec Inertia et Ziggy, on doit passer par Link. Du coup comment dire à PrimeVue de changer ?
C’est sur un forum perdu quelque part que j’ai trouvé la solution, ou du moins le bon point de départ. Il s’agit de définir un composant router-link personalisé qui utilise Link. En modifiant mon fichier app.js de mon projet Goo (voir l’article de mise en place) cela donne :
À ce stade nous n’avons pas encore utilisé route de Ziggy. D’ailleur si vous encapsulé le to dans un route(to), cela ne fonctionnera pas (le Menubar refuse même de s’afficher sans erreur :/). Du coup on déplace la transformation dans la définition du menu.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Menubar from 'primevue/menubar'
const menuItems = ref([
{
label: 'Factures',
icon: 'pi pi-fw pi-file',
to: route('invoices')
}
])
</script>
Et du coup nous voilà avec un menu PrimeVue qui fonctionne selon notre besoin Inertia et route Laravel.
Suite à un soucis de machine virtuelle, mon Vagrant m’a planté mon stack et Virtual Box ne s’en sort plus, je suis tombé en difficulté avec mon logiciel comptable fait maison : Goo (v2!). Une occasion de refaire un truc qui n’a pas bougé depuis ~15 ans :/, et de se mettre à jour sur différentes technos ou d’en découvrir de nouvelles.
Cet article a donc pour but de servir de tuto pour monter une nouvelle solution, là où de bons articles existent et m’ont servi (voir les sources au fur et à mesure), mais où ils n’ont pas forcément fait les mêmes choix ou le même montage final.
Laravel et Sail
On démarre avec Laravel, version 9 en ce moment (la 10 arrivera début d’année), et on va utiliser Sail pour l’installer. Sail nous apporte le confort conteneurisé de notre environnement de dev prêt à l’emploi avec les composants dont on peut avoir besoin (ex mySQL). Dans le contexte de Goo, on a 3 tables (clients, invoices, items), que j’ai modélisé avec Gleek, donc je pense qu’un SQLite sera largement suffisant.
Dans un WSL 2 debian, avec un Docker desktop démarré, et dans un répertoire de votre choix, je tape donc :
Le paramètre with permet d’indiquer les services dont on aura besoin, pour qu’il les prépare tout seul dans des containers. C’est bien pratique même si on en a pas besoin, du coup à vide j’évite d’avoir ceux par défaut, sauf que, malgré tout, il met mysql par défaut. On le retirera avant le premier sail up dans le docker-compose.
Ça prend du temps et c’est normal, même si vous avez déjà récupéré les images etc.
Vous aurez peut-être une erreur du style :
Mais ça passe quand même ainsi, je pense que c’est dû au with vide, car sans je n’ai pas vu le même message.
Comme dit juste avant, on va nettoyer notre docker-compose qui se trouve dans la racine de notre nouveau répertoire (ici goo3) et on va y retirer le bloc mysql, la ligne depends_on du bloc laravel.test et évidemment le volume de mysql en fin de fichier.
Quand on est prêt on va dans notre répertoire et on lance Sail :
cd goo3 && ./vendor/bin/sail up
Notre console nous montrera qu’il lance un container et dans docker desktop on le verra également.
Il ne nous reste plus qu’à lancer un navigateur et aller sur l’adresse indiquée ou localhost pour voir notre Laravel installé par défaut qui se lance proprement.
Comme vous pouvez le lire en bas-droite de l’image (si vous avez de bons yeux), on est bien en Laravel 9 sur un PHP 8.
Inertia, Vue, Ziggy et Tailwind
Il ne sera pas question ici de Breeze (paquet d’authentification bien foutu), je n’en ai pas besoin, du coup on ne profitera pas des Starter Kits proposés. Ce que l’on veut c’est Inertia, c’est à dire, un moyen d’avoir un framework front-end (React, Vue) en relation avec notre back-end PHP, non pas comme un back-end PHP qui serait un service REST (ex: Lumen) et un front-end qui le consomme, mais bien un back-end avec le rendu des pages côté back (SSR) et le dynamisme d’un Vue côté client une fois la page chargée. Mais on navigue bien d’une page à l’autre en passant par un appel back. Je trouve que cet entre-deux est intéressant pour les petites applications qui veulent se doter d’un front plus moderne sans devoir forcément sortir l’artillerie lourde. Je vous laisse lire la doc d’Inertia pour comprendre toute la mécanique, c’est bien expliqué.
Comme vous pourrez le voir dans votre fichier package.json nous avons déjà Vite et PostCSS. Vite remplace Mix et nous demande de nous adapter côté config. PostCSS sera utile pour l’installation de TailWindCSS.
Installation
Lançons nous ! On va exécuter une série de commandes pour installer Vue et Inertia ainsi que les dépendances nécessaires pour les lier. Pour ce faire je lance un Git Bash dans le répertoire du projet et j’ai Node 16 installé, il faut que Sail tourne. Et pour les commandes Sail je passe par la WSL (et/ou on fait tout en WSL si vous avez un Node 16 installé dedans).
Reprenons ce tas de lignes, on installe Vue côté front et ensuite Inertia côté back, puis côté front avec l’option Progress qui permettra de montrer les chargements de contenu XHR (AJAX olè). On ajoute Ziggy pour avoir le helper des routes côté front sur base de ce que le back a comme définitions (cf routes/web.php).
Pour la partie middleware, artisan va nous générer un fichier que l’on va pouvoir inclure dans notre config app/Http/Kernel.php :
On peut noter l’absence de title dans le head mais la présence du @inertiaHead qui nous permettra de jouer là dessus en fonction de la page affichée. @routes c’est Ziggy.
On va créer un répertoire dans resources/js : Pages et on va y ajouter une page : invoices.vue :
On va également créer une route pour cette page dans routes/web.php :
<?php
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/invoices', function () {
return Inertia::render('invoices', ['title' => 'Factures']);
})->name('invoices');
Donc on a Sail qui tourne, le run dev également, on peut se rendre sur http://localhost/invoices et voir le résultat :
Évidemment dans notre test on a hardcodé le titre et la réponse, on a pas découpé les composants, etc. Pas encore !
Layout
Plongeons dans cette question, et là un article, ainsi que la doc officielle vont nous aider. En gros on va créer un autre répertoire Layouts qui contiendra un fichier vue que l’on va appeler basic. Celui-ci contiendra quasiment tout ce que nous avions précédemment sauf Head et le contenu, ici un simple H1, qui sera remplacé par <slot/>.
À noter que les articles manquent de précision pour les novices en Vue, donc ils oublient de nous dire que la partie script est importante et manque dans leur exemple. Ce qui nous permet aussi de lui donner un nom explicite à l’usage. Notre page devient donc :
Relancez votre page et vous aurez le même résultat que précédemment, mais en mieux structuré. Le Head reste dans la page, on appelle le layout et on met notre contenu dedans, fin ! Simple !
Upgrade du Layout : Head title
Répéter le nom du site » – Goo 3″ dans toutes les pages n’est pas une bonne pratique du coup on peut imaginer passer le title à notre Layout qui centraliserait ce bloc de code.
On est un peu passé à côté, mais comme dit plus haut, pour une petite base de données de 3 tables nous n’avons pas besoin d’un gros système, pourquoi donc ne pas utiliser SQLite. La documentation de Laravel nous donne la solution simple. On va créer un fichier dans le répertoire database et déclarer son type dans notre config .env.
Pensez à virer les migrations qui ne vous intéressent pas avant d’exécuter la commande de migration d’artisan. Dans notre cas nous n’avons besoin que de nos 3 tables, du coup j’ai traduit mon schéma Gleek en migration Laravel.
Sanctum
Quand vous ferez votre migration, malgré le nettoyage, vous verrez apparaitre une migration en trop : c’est sanctum. Nous on ne l’utilisera pas, donc la solution, donnée dans la doc est d’ajouter une commande dans le register de l’appServiceProvider.
use Laravel\Sanctum\Sanctum;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
Sanctum::ignoreMigrations();
}
On sort des sentiers battu bien connu de MySQL avec un bon PHPMyAdmin en utilisant SQLite, du coup j’ai opté pour l’extension Chrome SQLite Manager. Et parmi les 3 extensions disponibles au moment décrire cet article, c’est la meilleure que j’ai testé.
PrimeVue vs Tailwind
D’abord se poser la question, est-ce qu’ils peuvent cohabiter et apporter leurs pierres à l’édifice ? La réponse en un article : oui ! L’idée étant que PrimeVue peut jouer le jeu en proposant ses composants sans utiliser les classes de Tailwind, mais en proposant un thème adapté, ce qui nous laisse champs libre pour un double usage. Il ne reste plus qu’à tenter son installation dans notre solution déjà bien aménagée.
Mais après quelques chipotages sur les priorités et les conflits générés voici la solution. On inverse et on met la base de Tailwind avant sinon celle-ci elle va écraser des styles de PrimeVue et on adapte l’import pour éviter le soucis de compilation.
Si vous faites le test avec un Button de PrimeVue, il était blanc de base et bleu au survol du fait d’une règle de background transparent. Maintenant c’est corrigé et le bouton est bien visible dès le début grâce à l’ordre des règles.
Tester c’est douter
Pour voir si c’est en ordre, j’ai simplement pris un composant simple ‘inputText’ dans ma page invoices.
Pas de mystère là dedans, un fichier assez classique avec en plus le plugin et l’inclusion des ressources à traiter. Il nous reste plus qu’à utiliser le mode typescript dans notre fichier invoices.vue par exemple.
On peut également changer de mode dans Vue et passer du mode options au mode composition. Essentiellement cela change la manière de concevoir vos composants. Pour ce faire, nous n’avons qu’à modifier notre fichier invoices.vue.
Ici j’ai remis l’exemple de l’inputText pour illustrer le principe et montrer que ça fonctionne. Notez l’attribut setup, le code en moins et la manière de déclarer une variable avec ref() et la méthode onMounted.
Conclusion, et ensuite ?
Ce fameux ensuite, car oui on peut toujours aller plus loin, certes, mais pour un tuto c’est déjà pas mal 🙂 On a quand même accompli quelques sujets. On a donc un back Laravel installé avec Sail, une dynamique de pages en Vue, Inertia et Ziggy, du Layout et un design Tailwindcss – PrimeVue. Si ça c’est pas joli ! Et en plus on a une base de données et du typescript, quelle affaire :p !
La suite c’est le développement de Goo 3 tel qu’énoncé en début d’article, mais ça, ça sera peut-être un autre article s’il y a de quoi en dire, car au final les points saillants ont déjà été abordés. Affaire à suivre !
[Bonus] Un petit favicon ?
J’ai découvert un chouette site pour générer facilement un favicon multi support. Pratique pour les projets d’entreprise par exemple. RedKetchup ont quelques outils sympa, je vous laisse les découvrir.
Pas de révolution ici, c’est quelque chose somme toute d’assez classique, un back-end NodeJs en Express et une doc Swagger basé sur un OpenAPI. Le truc étant de faire le routage et la doc en un seul point, améliorant la maintenabilité et automatisant le routage.
Pour ce faire il suffit de démarrer un nouveau projet et d’installer quelques librairies de base :
npm init
npm i express --save
npm i swagger-parser --save
npm i cors --save
npm i swagger-routes-express --save
Le framework express comme base,
La lib swagger-parser pour lire et interpréter le fichier openapi.yaml (définition de l’API),
La lib cors pour définir l’autorisation,
La lib swagger-routes-express pour créer un connecteur reliant les contrôleurs au routeur basé sur la définition de l’API,
La lib swagger-ui-express pour mettre à disposition un swagger de la définition de l’API, sur une route /api-docs.
Maintenant qu’on a la base il nous faut le squelette d’application, du coup au lieu de taper tout le code ici je t’invite cher lecteur à te rendre sur ce GitHub, et nous allons détailler les parties intéressantes.
OpenAPI
D’abord, qu’est-ce que l’on veut accomplir ? Dans cet exemple on va simplement faire un service REST pour obtenir une liste d’items, un GET. Nous allons donc décrire un fichier openapi.yaml décrivant cela.
openapi: 3.0.0
info:
description: service backend
version: 1.0.0
title: my-api
paths:
/items:
get:
summary: Get all items
description: Get all items
operationId: getItems
responses:
"200":
description: success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/item'
servers:
- url: /api/v1
components:
schemas:
item:
type: object
properties:
id:
type: number
name:
type: string
date:
type: string
En gros : on définit une route /items qui renverra un tableau d’item et la structure item. Jusque là c’est du formalisme standard que vous retrouverez un peu partout.
Le serveur
Ensuite il faut construire notre serveur (je n’utiliserai pas le générateur), pour cela nous aurons principalement 2 fichiers de base et un fichier contrôleur. Le schéma étant assez simple, on reçoit une requête, le routeur fait un match et associe la méthode du contrôleur.
On commence par écrire un fichier makeApp.js (dans /src), qui correspond à la définition de notre serveur, notre application. Dedans on y trouve beaucoup de choses, on y reviendra plus en détail après, mais en gros on décrit ce que nous avons dit plus haut, à savoir : définir un serveur express auquel on va connecter le routeur, lui-même basé sur le fichier openapi.yaml décrit plus haut. Le connecteur du routeur se lie aux contrôleurs, nous y reviendrons.
Ensuite on a le fichier point de départ : index.js, que l’on placera dans un répertoire /src, du coup n’oubliez pas de modifier package.json avec la propriété main :
"main": "./src/index",
Ainsi que la commande start, celle ci se lancerait avec un npm run start mais vous pourriez avoir une surprise, via un Git Bash sur Windows, d’avoir une erreur sur le ./
"scripts": {
"start": "./src/index.js",
Dans ce fichier index.js on aura un appel au précédent fichier de config.
const makeApp = require('./makeApp')
const port = 3000;
makeApp()
.then(app => app.listen(port))
.then(() => {
console.log(`App running on port ${port}...`)
})
.catch(err => {
console.error('caught error', err)
})
Le contrôleur
Nous avons vu que le serveur se lie aux contrôleurs, il est maintenant temps de nous y intéresser. Nous allons créer un répertoire controllers dans /src, et un fichier item.js permettant de regrouper par domaine les actions concernant les items. À côté on créer un fichier index.js permettant de lister le contenu du répertoire au niveau de l’appelant, une façon de faire également utilisée en typescript/Angular.
On peut voir la fonction getItems dont le nom match l’attribut operationId du fichier openapi.yaml. Évidemment c’est voulu 🙂 Et c’est important de garder ça à l’œil quand vous préparez votre YAML. Actuellement la méthode renvoie un tableau vide.
Compléter le serveur
Avant de vouloir tester il nous manque un morceau, en fait plusieurs petits détails. Pour faire des appels à notre API, depuis notre poste, on aura un soucis de CORS, mais aussi, potentiellement de cache (en-tête etag) et d’url encoding. À cela on veut préciser que l’on traitera du JSON dans les échanges.
Pour tester on peut lancer le serveur avec : node src/index.js
On peut ouvrir un Bash et lancer un curl de test, tel que :
$ curl -s localhost:3000/api/v1/items
[]
Du coup bonne nouvelle on a quelque chose qui fonctionne et répond correctement, à savoir une réponse d’un tableau vide ([]). Pour le fun on peut structurer une donnée et la renvoyer, en respectant le modèle proposé dans le YAML.
Ensuite on va ajouter les lignes suivantes dans notre makeApp.js, l’une dans les déclarations, l’autre après les options afin de déclarer une route de documentation et lancer Swagger.
const express = require('express')
const swaggerUi = require("swagger-ui-express");
...
// This is the endpoint that will display the swagger docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(apiDescription));
Couper/relancer le serveur et rendez-vous sur l’url localhost:3000/api-docs.
Et ensuite ?
À partir de là libre à vous de créer des services, connecter une base de données, gérer du fichier, etc.
Je sors d’une mission dans laquelle j’ai été amené à travailler avec PrimeNg, qui propose dans un même framework tout ce dont on a besoin. Évidemment c’est joli sur papier mais dans la réalité ce n’est pas le cas, PrimeNg souffre d’un manque de flexibilité, mais ils ont tous ce soucis, donc bon, on fait avec.
Mise en place
Ici je vais vous parler du cas des tableaux, plus précisément de la partie rendu et composant de cellule, ce que AG Grid propose mais pas PrimeNg. Donc à nous d’ajouter une couche. On va créer un projet de POC avec ce dont on aura besoin (vous avez node lts, npm et un angular/cli) :
ng new primeng-simple-table
cd primeng-simple-table
npm i primeng-lts primeicons --save
npm i primeflex --save
npm i @angular/cdk@12 --save
Dans notre app.module.ts on va ajouter l’import du TableModule
On va vider app.component.html, puis créer un composant qu’on va baptiser <simple-table> et dedans on va y mettre le code de base du tableau PrimeNg, dans sa version dynamique.
<p-table [columns]="cols" [value]="data" responsiveLayout="scroll">
<ng-template pTemplate="header" let-columns>
<tr>
<th *ngFor="let col of columns">
{{ col.header }}
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-rowData let-columns="columns">
<tr>
<td *ngFor="let col of columns">
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
</p-table>
Notez les modifications de nom de variables et notre fichier .ts .
On compile, on regarde le résultat… vous n’avez pas grand chose !
Des colonnes dynamiques
On faisant un composant dynamique on perd la faculté de définir les colonnes manuellement, c’est le but, on veut un système générique qui le fasse pour nous suivant ce qu’on lui donne, on va lui donner le moyen de créer les colonnes désirées, et on va en profiter pour se créer un jeu de données pour les tests.
On se crée donc une interface de définition de colonne et on passera un tableau de ColumnDef à notre composant pour définir ce que nous voulons. Dans ce premier jet nous avons l’attribut de donnée à utiliser pour la cellule (field) et le titre de colonne (header).
Jeu de données
Avant de mettre en pratique et jouer avec un exemple nous allons nous créer un jeu de données, prenons des vaisseaux spatiaux, au grand hasard. Voici la structure utilisée, déposé dans un fichier annexe qu’on importera :
Sur base de notre jeu de données et de l’interface ColumnDef nous allons définir nos colonnes pour notre tableau. J’en profite pour typer les variables cols en fonction. Dans app.component.ts nous modifierons tel que :
Magnifique, jusque là on a pas fait grand chose, PrimeNg nous donne la solution et nous avons juste typé plus proprement l’attribut column qu’ils proposent.
Rendu de cellule
Maintenant que nous avons une démo qui fonctionne avec des colonnes dynamiques, voyons ce qu’est le rendu de cellule qui nous manque. Prenez un premier cas avec la valeur status de notre jeu de données, nous voulons afficher un texte compréhensible, et non la valeur de l’enum, en fonction de l’état du vaisseau.
De base rien ne nous le permet, regardez la partie {{ rowData[col.field] }}, ceci ne fait qu’imprimer la valeur de l’attribut courant. Il faut se mettre entre la valeur et son rendu. Une solution : une méthode intermédiaire, optionnelle, et variable s’il vous plaît. Pour ce faire nous allons ajouter un attribut à notre interface ColumnDef.
Désormais on peut lui donner une fonction qui recevra en paramètre l’entièreté des données de la ligne courante ainsi que le champs courant à rendre. Modifions notre définition pour cette colonne :
Jusque là ça ne change rien, il nous faut l’appliquer. Pour ce faire il y a 2 chose : que faire de ceux qui n’ont pas de méthode définie, et comment appliquer cette méthode. Pour la première partie nous allons définir une méthode par défaut sous forme de service et l’appliquer à la place de l’utilisateur à l’initialisation du composant. Pour la seconde nous utiliserons un container et la propriété [innerHTML].
Le service
Ce service contiendrait les solutions fournies de base, rien ne sert de réinventer la roue à chaque projet :).
import { Injectable } from "@angular/core"
@Injectable({
providedIn: 'root'
})
export class CellRenderer {
static none(rowData: any, field: string): string {
const value: any = rowData ? rowData[field] : false
if (value || value === 0) {
return value
}
return ''
}
}
Modification de l’@Input cols
On transforme notre variable en privée avec setter/getter pour avoir la main dessus en cas de modifications et pouvoir ainsi mettre les valeurs par défaut. On aide ainsi l’utilisateur de notre service à ne pas se casser la tête avec toutes nos options :).
Il ne nous reste qu’à modifier notre rendu de valeur au sein de la cellule.
<td *ngFor="let col of columns">
<div [outerHTML]="col.renderer(rowData, col.field)"></div>
</td>
Et nous voilà avec un rendu de cellule dynamique. Ceci n’est qu’un exemple, ce qui m’est arrivé le plus souvent étant une transformation de valeur booléenne ou de date partant d’un format variable (2022-03-15 ou un timestamp) pour aller vers une représentation francophone (15/03/2022). Dans le cas de la date, la fonction du CellRenderer appel un service spécifique central, qui pourra être appellé par un pipe ou autre service.
Composant de cellule
Au lieu d’une modification de texte, nous souhaitons parfois avoir un comportement, une interaction, avec la donnée ou la ligne, tel qu’un bouton, une liste ou autre type de composant. Ceux-ci ont des paramètres et des événements et il faut arriver à les glisser dynamiquement dans une cellule.
Nous allons jouer avec la colonne type en modifiant data pour l’exemple et voir comment un rendu de cellule aurait pu nous aider (ce n’est pas la seule solution).
Mais ici on va directement attaquer un gros morceau en transformant cette cellule en liste déroulante pour modifier le type du vaisseau, soyons fou. Pour ce faire il va nous falloir plusieurs morceaux :
Définir ce qu’est un composant par des interfaces,
Modifier l’interface ColumnDef pour ajouter une définition pour composant,
Créer le composant liste
Décrire une mécanique permettant d’insérer un composant au sein de la cellule,
Modifier le rendu de la cellule pour détecter s’il s’agit d’un composant ou d’un renderer.
Modifier la définition des colonnes pour intégrer les changements
Création des interfaces
De quoi avons-nous besoin ? De dire quel composant il s’agit, de pouvoir lui passer des options et de lui donner le moyen de nous revenir par des événements.
export interface CellComponent {
component: any
options?: any
events?: any
}
Jusque là c’est assez générique, on lui dit qu’on pourra faire quelque chose, fort optionnel, et on spécifiera avec notre cas d’usage : une liste.
On aura besoin de lui donner la liste des options de la liste déroulante, et quel attribut utiliser comme valeur et comme texte à afficher. En retour nous serons alerté en cas de changement en nous retournant l’événement original du composant, ainsi que les données de la ligne et la colonne courante, ça peut toujours servir de donner les infos que l’on a.
Modification de l’interface ColumnDef
Comme nous avons nos types nous pouvons maintenant modifier notre interface de départ pour ajouter l’option composant.
Au pluriel car une colonne pourrait contenir plus d’un bouton par exemple, laissons de la flexibilité.
Créer le composant liste
Dans un répertoire /components/cell-components j’ai créé le composant list. Nous nous baserons sur le composant Dropdown de PrimeNg, avec leur exemple custom content, ce n’est pas obligé, mais si vous allez dans le détail ça sera plus facile d’avoir déjà la main sur le rendu via les templates. Du coup en avant dans l’HTML. N’oublier pas d’ajouter DropdownModule et ses dépendances.
Sur base de l’exemple qui parle de country, j’ai mis les nommer les variables que l’on va définir et nettoyer l’exemple, les détails comptent. La suite se passe dans le fichier .ts pour définir tout ce beau monde.
@Component({
selector: 'cc-list',
templateUrl: './list.component.html'
})
export class ListComponent implements OnInit {
// Must be in a parent class
options?: CellComponentOptionsList // Specific to list, any for generic
events?: CellComponentEventsList
rowData!: any
field!: string
// Specific to list
items: any[] = []
itemValue!: string
itemLabel!: string
value?: string
ngOnInit(): void {
}
}
Alors d’abord, si on avait plusieurs composant de cellule on aurait forcément une classe parente avec la partie commune, pour ce POC j’ai regroupé et annoté. Ensuite, vu qu’on viendra d’une cellule il sera normal et attendu que l’on passe ce que la cellule a à nous donner, à savoir rowData et field, ainsi que la définition spécifique à notre composant (options et events), venant de la config dans ColumnDef. Enfin, comme définit dans nos interfaces, les particularités spécifiques à la liste (items, itemValue et itemLabel). Notez encore la propriété value qui contiendra le choix courant et qui sera utilisé dans l’événement change.
Tel quel, le composant ne fera qu’exister avec une liste vide et les paramètres non-défini. Nous les ajouterons après pour que vous compreniez d’abord bien le chainage des attributs et les affectations. Restons simple, ça se complexifiera vite assez.
La mécanique d’insertion du composant dans la cellule
C’est là tout le truc, en un point du code HTML on doit dire à Angular : « je veux mon composant là et que tu le raccordes à mes options, événements, … ». Pour ce faire on va passer par une directive qui jouera le rôle d’hôte.
Pour faire simple, cette directive récupère ce qu’on lui passe sous forme d’attributs : cellComponentsHost, rowData et field. Attention au casting ListComponent, là c’est la classe parent que l’on indique normalement. Ensuite elle crée le composant et le place dans le container sur lequel est placé la directive. N’oubliez pas de la déclarer.
<td *ngFor="let col of columns">
<ng-container *ngIf="!col.components"><div [outerHTML]="col.renderer(rowData, col.field)"></div></ng-container>
<ng-container *ngIf="col.components" [cellComponentsHost]="col.components" [rowData]="rowData" [field]="col.field"></ng-container>
</td>
Pas de chipotage, on encapsule notre rendu de cellule précédent dans un container conditionnel, dans l’autre on y retrouve notre directive et les attributs qu’on lui passe.
On lui indique donc le composant voulu et nos options, jusque là vous devriez pouvoir faire les liens :), et on sort en console pour vérifier que l’évent a donné quelque chose.
Compléter le composant liste
Nous avons une interface de définition et nous avons définit notre cas d’utilisation sur la colonne type. Ensuite, nous avons créé le composant sans l’implémenter, ainsi que la directive hôte. Enfin, nous avons mis en place l’usage de la directive. On pourrait tracer le cheminement ainsi :
Le tableau boucle sur les colonnes pour créer une ligne.
Il arrive sur une cellule et se pose la question : est-ce un composant ou non ?
Si non : il tente le rendu de cellule (none par défaut)
Si oui :
La directive est fournie des informations et se lance pour créer chaque composants demandés en son seing
Le composant nouvellement créé est prêt
Il est prêt ? Qu’est-ce que cela veut dire ? On a parlé de compléter le composant pourtant :/ … et en effet on pourrait s’arrêter là et utiliser options directement, excepter le onChange manquant, mais nous désirons peut-être appliquer des contrôles ou des détections préliminaires, et dans cette idée je vais vous montrer une possibilité.
Il est beau, il est joli, mais est-ce tout ? Seule l’imagination étant notre limite, la réponse évident est : non ! Tout dépend de votre besoin, de vos envies de flexibilités, de ce que vous souhaitez proposer. Pour ma part voici des pistes abordées et développées :
Reporter les options fournies par p-table
Ce n’est pas une évidence pour tout le monde, mais en dehors des choix fait pour votre composant (imposer le scroll, le système de colonnes, …) vous bloquez l’accès aux autres options proposées par p-table, du coup ceux qui veulent utiliser votre composant vont être bloqués/frustrés. Une bonne pratique est de faire des ponts pour reporter ces options autant que possible, merci le code source de la lib pour vous aider.
Ligne de filtres
PrimeNg permet d’ajouter des filtres mais en dynamique il vous faudra user d’une autre directive hôte et d’une interface, sans oublie l’@Input pour passer votre demande au composant.
Sélection
PrimeNg permet également de mettre en place un système de sélection de ligne(s), mais vu notre côté dynamique c’est à nous de réintégrer cette solution, de manière optionnelle. Ceci sous-entend d’ajouter des colonnes de sélection et de donner à p-table la configuration (none, single, multiple)
Classes CSS
On l’oublie souvent, mais le design ! Du coup vous voulez préciser pour telle colonne un style (classe CSS), il faudra faire évoluer la définition de colonnes et modifier l’HTML de notre composant pour l’intégrer dans la partie template, tant sur l’en-tête que sur la cellule. On peut également imaginer que l’en-tête et la cellule peuvent avoir des styles différents (couleur, alignement, …).
Le composant liste
Notre composant liste peut également être poussé plus loin, en activant le filtre et/ou en améliorant le rendu pour afficher un contenu plus complexe comme un préfix à la valeur, ce qui m’est souvent arrivé : [PRE – valeur affichée] .
Dans le dernier article nous avions notre composant qui s’occupait de s’extraire et une directive qui s’occupait de le remettre dans une boite, en gros. Mais bon, si le composant pouvait éviter de s’extraire pour être remis en boite ça serait quand même mieux.
Du coup l’idée est de lui rendre la notion « es-tu une ligne ? » via un @Input() row: boolean = true; En considérant qu’il l’est par défaut (au choix). Ensuite quand on crée le composant on agit selon la valeur de row.
À l’initialisation, si nous somme une ligne, nous allons transformer notre template enfant #tref en contenu de notre propre bloc via le ViewContainerRef pour le créer et le Renderer2 qui va l’injecter dans le nœud de notre composant courant. Ensuite on peut signaler à notre bloc courant, le composant en lui-même, que nous aimerions le décorer de quelques classes CSS pour en faire une row css au sens Bootstrap.
if (this.row) {
// Transform template into inside elements
Promise.resolve(null).then(() => {
const view = this.vcRef.createEmbeddedView(this.tpl);
view.rootNodes.formEach((node) => this.renderer.appendChild(this.el.nativeElement, node));
});
// Transform parent to row with classes
this.renderer.setAttribute(this.el.nativeElement, 'class', "row " + this.classes);
}
Quand l’élément est prêt, et que nous ne sommes pas une ligne, on continue de sortir notre contenu en dehors du bloc par la même mécanique. On ajoute une classe css au bloc courant pour le masquer (bootstrap: d-none -> display: none) et par la même mécanique que le paragraphe précédent on utilise le template et l’ajoute un cran plus haut du coup. Aussi simple que ça.