ESLint, un emmerdeur qui a souvent raison

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 :

/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")

module.exports = {
    extends: [
        'eslint:recommended',
        'plugin:vue/vue3-essential',
        '@vue/eslint-config-typescript'
    ]
}

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.

export interface ColDef {
    field: string
    header: string
    renderer?: (rowData: {[key: string]: any}, field?: string) => string
    sortable?: boolean
    components?: CellComponent[]
}

export interface CellComponent {
    component: any
    props?: Object
    on?: Object
    model?: Object
}

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.

...
    colsDef: {
        type: Array as PropType<ColDef[]>,
        required: true,
        default: () => []
    },
...
    rowClass: {
        type: Function,
        default: (data: any) => '' // Cf primevue github
    }
...

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.

...
const cols: ComputedRef<ColDef[]> = computed<ColDef[]>(() => {
    return props.colsDef!.map((c) => ({
...
    }) as ColDef)
})

const interceptEvents = (events: { [key: string]: Function } | undefined, rowData: Object, field: string): Object => {
    if (!events) {
        return {}
    }

    const interceptedEvents: { [key: string]: Function } = {}
    Object.keys(events).forEach((e) => {
        interceptedEvents[e] = (ev: Object) => {
            return events[e](ev, rowData, field)
        }
    })
    return interceptedEvents
}
...

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.

PrimeVue : rendu et composant de cellule

Cette article donne suite à l’article PrimeNg vs AG Grid : rendu et composant de cellule sur une continuité de l’article Goo 3 avec Laravel 9, Sail, Inertia, Vue3, tailwindcss, Vite et PrimeVue.

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.

Le tableau avec rendu de cellule

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.

<template>
    <DataTable :value="props.data" sortField="props.defaultSort" :sortOrder="-1" responsiveLayout="scroll"
               stripedRows :rowClass="props.rowClass">
        <Column v-for="c in cols" :field="c.field" :header="c.header" :sortable="c.sortable">
            <template #body="colProps">
                {{ c.renderer(colProps.data, c.field) }}
            </template>
        </Column>
    </DataTable>
</template>

En option j’ai ajouté le célèbre zèbre de lignes et un argument rowClass, qui permet de préciser une méthode pour faire varier le design de la ligne.

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.

<script setup lang="ts">
import {computed, ComputedRef, PropType} from "vue";
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import {ColDef} from "../Models/table";
import {CellRenderer} from "../Services/cell-renderer";

const props = defineProps({
    data: {
        type: Array,
        required: true
    },
    colsDef: {
        type: Array as PropType<ColDef[]>,
        required: true,
        default: []
    },
    defaultSort: {
        type: String,
        required: false,
        default: 'id'
    },
    rowClass: {
        type: Function,
        default: (data) => ''
    }
})

const cols: ComputedRef<ColDef[]> = computed<ColDef[]>(() => {
    return props.colsDef.map((c) => ({
        ...c,
        sortable: c.sortable == null ? false : c.sortable,
        renderer: c.renderer == null ? CellRenderer.none : c.renderer
    }))
})
</script>

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 :

export interface ColDef {
    field: string
    header: string
    renderer?: (rowData: Object, field?: string) => string
    sortable?: boolean
}

La class du CellRenderer est la même que dans la version Angular.

Appel du composant et configuration

Pour l’appel c’est simple, on mentionne notre composant en lui passant les paramètres que l’on veut. Classique quoi, mais en Vue.

<template>
...
    <simple-table
        :cols-def="colsDef"
        :data="fakeInvoices"
        default-sort="created_at"
        :row-class="rowClass"
    ></simple-table>
...
</template>

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.

<script lang="ts" setup>
import BasicLayout from '../Layouts/basic.vue'
import Button from 'primevue/button';
import SimpleTable from "../Components/simple-table.vue";
import {Ref, ref} from "vue";
import {Invoice} from "../Models/invoice";
import {ColDef} from "../Models/table";
import {CellRenderer} from "../Services/cell-renderer";

const fakeInvoices: Ref<Invoice[]> = ref([
    {
        num: '123416789',
        client_id: 1,
        status: 1,
        client: {
            name: 'Client 1'
        },
        created_at: '2023-01-01T13:16:12.000000Z'
    },
    {
        num: '124518963',
        client_id: 2,
        status: 0,
        client: {
            name: 'Client 2'
        },
        created_at: '2023-01-01T13:16:12.000000Z'
    }
])

let colsDef: Ref<ColDef[]> = ref([
    {
        field: 'num',
        header: 'N°',
        sortable: true
    }, {
        field: 'client_id',
        header: 'Client',
        sortable: true,
        renderer: (rowData, field) => {
            return rowData?.client.name
        }
    }, {
        field: 'created_at',
        header: 'Crée le',
        sortable: true,
        renderer: CellRenderer.date
    }, {
        field: 'status',
        header: 'Statut',
        sortable: true,
        renderer: (rowData, field) => {
            return (rowData[field]) ? 'Payé' : 'Non-payé'
        }
    }
])

function rowClass(data: Invoice): string {
    return data.status ? 'paid' : 'unpaid'
}
</script>

Composant de cellule

On va d’abord modifier l’interface colDef pour intégrer cette histoire de composant.

export interface ColDef {
    field: string
    header: string
    renderer?: (rowData: Object, field?: string) => string
    sortable?: boolean
    components?: CellComponent[]
}

export interface CellComponent {
    component: Object
    props?: Object
    on?: Object
    model?: Object
}

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.

...
        <Column v-for="c in cols" :field="c.field" :header="c.header" :sortable="c.sortable">
            <template #body="colProps" v-if="c.components">
                <component v-for="comp in c.components"
                           :is="comp.component"
                           v-bind="comp.props"
                           v-on="comp.on"
                ></component>
            </template>
            <template #body="colProps" v-else>
                {{ c.renderer(colProps.data, c.field) }}
            </template>
        </Column>
...

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.

...
    }, {
        field: '',
        header: 'Actions',
        components: [
            {
                component: markRaw(Button),
                props: {
                    class: 'p-button-rounded p-button-secondary',
                    icon: 'pi pi-user'
                },
                on: {
                    click: (e) => {
                        console.log(e)
                    }
                }
            }
        ]
    }
...

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.

Le bouton s’affiche bel et bien en lieu et place
Contenu de l’événement renvoyé quand on clique sur le bouton

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.

...
                <component v-for="comp in c.components"
                           :is="comp.component"
                           v-bind="{...comp.props, rowData: colProps.data, field: c.field}"
                           v-on="interceptEvents(comp.on, colProps.data, c.field)"
                ></component>
...
...
const interceptEvents = (events: Object, rowData: Object, field: string): Object => {
    const interceptedEvents = {}
    Object.keys(events).forEach((e) => {
        interceptedEvents[e] = (ev) => {
            return events[e](ev, rowData, field)
        }
    })
    return interceptedEvents
}
...

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 :

...
                on: {
                    click: (e, rowData: Invoice, field: string) => {
                        console.log(e, rowData, field)
                    }
                }
...
On observe bien 3 paramètres logué dans la console.

Sources additionnelles

Conclusion

Nous voilà donc avec un tout nouveau, mais non-réinventé, composant tableau dynamique générique, en Vue pour le coup.

Reste à développer ou explorer les limites pour d’autres types de composants à envoyer au tableau.

PrimeVue, Inertia et Ziggy : un lien pour naviguer

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 :

createInertiaApp({
    resolve: async (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
            .use(PrimeVue)
            .component("Link", Link)
            .component("Head", Head)
            .component("router-link", {
                props: ["to","custom"],
                template: `<Link :href="to"><slot/></Link>`,
            })
            .mixin({ methods: { route } })
            .mount(el);
    },
});

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

Goo 3 avec Laravel 9, Sail, Inertia, Vue3, tailwindcss, Vite et PrimeVue

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 :

sudo curl -s "https://laravel.build/goo3?with=" | bash

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

Pour la suite on va bricoler entre deux articles : Setting up Laravel with Inertia.js + Vue.js + Tailwind CSS et Migrating Laravel 9 with Inertia.js + Vue.js + Tailwind CSS from Laravel Mix (Webpack) to Vite, en gros l’installation en Laravel 8 puis l’upgrade vers la version 9. Évidemment on va directement le faire en 9.

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

npm install vue@next
./vendor/bin/sail composer require inertiajs/inertia-laravel
./vendor/bin/sail php artisan inertia:middleware
npm install @inertiajs/inertia @inertiajs/inertia-vue3
npm install @inertiajs/progress
npm i --save-dev @vitejs/plugin-vue
./vendor/bin/sail composer require tightenco/ziggy

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 :

    protected $middlewareGroups = [
        'web' => [
...
            \App\Http\Middleware\HandleInertiaRequests::class,
        ],

Il nous reste à faire en sorte que tout ce petit monde installé travaille ensemble, mais avant on va mettre TailWindCSS tant qu’à faire.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
npm install --save-dev postcss-import

On installe Tailwind et ses dépendances. De plus on initialise un fichier de config pour Tailwind : tailwind.config.js à la racine.

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ["./resources/js/**/*.{vue,js}"],
    theme: {
        extend: {},
    },
    plugins: [],
};

Dans le fichier resources/css/app.css on ajoute les imports Tailwind :

@tailwind base;
@tailwind components;
@tailwind utilities;

On modifie notre fichier vite.config.js pour ajouter vue, Ziggy et retirer l’input css :

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.js'],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
    resolve: {
        alias: {
            'ziggy': '/vendor/tightenco/ziggy/src/js',
            'ziggy-vue': '/vendor/tightenco/ziggy/src/js/vue'
        },
    },
});

On crée un fichier postcss.config.js :

module.exports = {
    plugins: [
        require('postcss-import'),
        require('tailwindcss')
    ]
}

On a également besoin que Ziggy génère son fichier, qu’on mettra à jour au fur et à mesure avec la même commande :

./vendor/bin/sail php artisan ziggy:generate resources/js/ziggy.js

Et enfin, on supprime le fichier bootstrap.js de resources/js, et on édite le fichier app.js avec la totale :

import { createApp, h } from "vue";
import { createInertiaApp, Link, Head } from "@inertiajs/inertia-vue3";
import { InertiaProgress } from "@inertiajs/progress";
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

import { ZiggyVue } from "ziggy-vue";
import { Ziggy } from "./ziggy";
import '../css/app.css';

InertiaProgress.init();

createInertiaApp({
    resolve: async (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue, Ziggy)
            .component("Link", Link)
            .component("Head", Head)
            .mixin({ methods: { route } })
            .mount(el);
    },
});

En dehors de devoir relancer la création du fichier de Ziggy pour les routes à jour, on peut lancer la compilation :

npm run dev

Évidemment il ne se passera rien de notable… nous n’avons pas de page vue avec le body qui va bien etc.

Une page de test

On va créer notre body en renommant le fichier resources/views/welcome.blade.php en app.blade.php avec pour contenu :

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    @routes
    @vite('resources/js/app.js')
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>

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 :

<template>
    <Head>
        <title>{{ $page.props.title }} - Goo 3</title>
    </Head>

    <div class="p-6">
        <div class="flex space-x-4 mb-4">
            <Link
                :href="route('invoices')"
                class="text-gray-700 bg-gray-200 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
                >Homepage</Link
            >
        </div>

        <h1>This is: {{ $page.props.title }}</h1>
    </div>
</template>

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

<template>
    <div class="p-6">
        <div class="flex space-x-4 mb-4">
            <Link
                :href="route('invoices')"
                class="text-gray-700 bg-gray-200 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
                >Homepage</Link
            >
        </div>

        <slot/>
    </div>
</template>

<script>
export default {
  name: 'BasicLayout',
};
</script>

À 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 :

<template>
    <Head>
        <title>{{ $page.props.title }} - Goo 3</title>
    </Head>

    <basic-layout>
        <h1>This is: {{ $page.props.title }}</h1>
    </basic-layout>
</template>

<script>
import BasicLayout from '../Layouts/basic.vue';

export default {
  components: {
    BasicLayout,
  },
};
</script>

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.

<template>
    <Head>
        <title>{{ title }} - Goo 3</title>
    </Head>

    <Menubar :model="menuItems">
        <template #start>
            <h1>Goo</h1>
        </template>
    </Menubar>

    <main class="container mx-auto p-6">
        <slot/>
    </main>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import Menubar from 'primevue/menubar'

defineProps<{
  title?: string
}>()

const menuItems = ref([
        {
            label: 'Factures',
            icon: 'pi pi-fw pi-file',
            to: route('invoices')
        }
    ])
</script>

Et son appel :

<template>
    <basic-layout :title="$page.props.title">

    </basic-layout>
</template>

<script lang="ts" setup>
import BasicLayout from '../Layouts/basic.vue'
</script>

La base de données

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();
    }

Notez que le use n’est précisé nul part, merci StackOverflow.

SQLite Manager

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

Une fois le fichier chargé on peut voir la liste des tables et de quoi faire des requêtes.

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.

npm install primevue@^3 --save
npm install primeicons --save

Ensuite dans le fichier app.js on ajoute ceci ensemble.

...
import '../css/app.css';

import PrimeVue from 'primevue/config';
import 'primevue/resources/themes/tailwind-light/theme.css';
import 'primevue/resources/primevue.min.css';
import 'primeicons/primeicons.css';

InertiaProgress.init();
...

Pour déplacer les imports CSS dans notre app.css il faudra gruger comme ceci :

@import '/node_modules/primevue/resources/themes/tailwind-light/theme.css';
@import '/node_modules/primevue/resources/primevue.min.css';
@import '/node_modules/primeicons/primeicons.css';

@tailwind base;
@tailwind components;
@tailwind utilities;

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.

@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

@import '/node_modules/primevue/resources/themes/tailwind-light/theme.css';
@import '/node_modules/primevue/resources/primevue.min.css';
@import '/node_modules/primeicons/primeicons.css';

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.

<template>
    <Head>
        <title>{{ $page.props.title }} - Goo 3</title>
    </Head>

    <basic-layout>
        <h1>This is: {{ $page.props.title }}</h1>

        <span class="p-input-icon-right">
            <InputText type="text" v-model="search" />
            <i class="pi pi-spin pi-spinner" />
        </span>
    </basic-layout>
</template>

<script>
import BasicLayout from '../Layouts/basic.vue';
import InputText from 'primevue/inputtext';

export default {
  components: {
    BasicLayout,
    InputText,
  },
  data() {
    return {
      search: 'coucou'
    }
  },
};
</script>

Évidemment ne gardez pas ça, ce n’est qu’un test pour vérifier que tout fonctionne ^^.

Typescript

Allez on ajoute une couche et on passe en Typescript 🙂 !

npm i typescript @vuedx/typescript-plugin-vue --save-dev

Ensuite on ajoute un fichier de config tsconfig.json :

{
    "compilerOptions": {
        "target": "esnext",
        "module": "esnext",
        "moduleResolution": "node",
        "strict": true,
        "jsx": "preserve",
        "sourceMap": true,
        "resolveJsonModule": true,
        "esModuleInterop": true,
        "lib": [
            "esnext",
            "dom"
        ],
        "plugins": [
            {
                "name": "@vuedx/typescript-plugin-vue"
            }
        ]
    },
    "include": [
        "resources/js/**/*.ts",
        "resources/js/**/*.d.ts",
        "resources/js/**/*.vue"
    ]
}

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.

...

<script lang="ts">
import { defineComponent } from 'vue'
import BasicLayout from '../Layouts/basic.vue';

export default defineComponent({
  components: {
    BasicLayout,
  },
})
</script>

On notera l’attribut lang= »ts » qui précise comment traiter ce segment, ensuite suivant la doc on doit utiliser defineComponent.

Vue composition API

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.

<template>
    <Head>
        <title>{{ $page.props.title }} - Goo 3</title>
    </Head>

    <basic-layout>
        <h1>This is: {{ $page.props.title }}</h1>

        <span class="p-input-icon-right">
            <InputText type="text" v-model="search" />
            <i class="pi pi-spin pi-spinner" />
        </span>
    </basic-layout>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import BasicLayout from '../Layouts/basic.vue'
import InputText from 'primevue/inputtext'

const search = ref('coucou');

// lifecycle hooks
onMounted(() => {
    setTimeout(() => {
        search.value = 'test'
    }, 2000);
})
</script>

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.