// BLOG

Composants Svelte + Custom Element + Vite + Tailwind CSS

Svelte est généralement utilisé pour créer des Single Page Application (SPA) ou des applications Web avec du Server Side Rendering (SSR) via Sveltekit. Mais vous pouvez aussi utiliser Svelte pour créer des composants web (ou des Custom Element) "universels" qui viennent s'intégrer dans une application existante qu'elle soit programmée en PHP, Python ou autre. Dans cet article, je vais vous montrer comment faire en utilisant Vite et en intégrant le framework CSS Tailwind pour styler vos composants.

Rédigé le 19.04.2022
Par Gilles Vauvarin

Svelte Web Component Custom Element Vite TailwindsCSS

Contexte

Mon outil de prédilection pour développer des sites web ou des petites applications web est Kirby CMS. C'est un CMS flatfile, très performant, agréable à utiliser et bien documenté. Ce que j'apprécie particulièrement c'est qu'il nous laisse la liberté de construire un espace d'administration "from scratch" pour l'adapter au contenu de notre projet.

Pour ce qui est des interactions, j'ai décidé d'utiliser des composants web Svelte plutôt qu'une librairie comme AlpineJS ou Petite-Vue. Les composants sont plus facilement réutilisables, peuvent être documenté dans un outil comme Storybook pour concevoir un design system et facilitent la lisibilité et la maintenabilité du code.

Une fois mes composants crées, je peux les appeler simplement dans des templates Kirby, des snippets Kirby ou au sein de blocks du Page Builder de kirby.

Installation de l'environnement

Je pose comme prérequis que vous avez une version NodeJS 12.2.0 minimum et une version npm 7.0 minimum installé. J'utilise npm dans cet article mais vous pouvez biensure utiliser un autre gestionnaire de package comme Yarn ou pnpm si vous le souhaitez. Je considère aussi que vous avez des notions de Svelte et Tailwind CSS.

Svelte et Vite

Pour créer nos composants web grâce à Svelte, nous allons avoir besoin de créer un projet Svelte. J'utilise ici Vite comme outil de build et un template Svelte prêt à l'emploi mis à disposition par la communauté Vite.

npm create vite@latest my-svelte-app -- --template svelte

# Remplacez "my-svelte-app" par l'intitulé qui vous convient

Ensuite vous pouvez vous placez dans votre dossier "my-svelte-app" pour installer les dépendances :

# Je me place dans le dossier fraîchement créé par l'instruction précédente

cd my-svelte-app

# J'installe les dépendances

npm install

Tailwind CSS et PostCSS

# J'installe le package Tailwindcss

npx svelte-add@latest tailwindcss

# J'installe les dépendances

npm install

Cette commande installe Tailwind CSS, PostCSS et paramètre les fichiers de configuration correspondand tailwind.config.cjs , postcss.config.cjs Pour rappel, PostCSS est un outil JavaScript pour manipuler du CSS (minification, ajout de prefix, linting ...).

Notre environnement de travail est maintenant prêt. Nous allons pouvoir concevoir nos composants Svelte.

Création d'un composant web Svelte

Le composant

Si tout s'est bien passé, vous devriez avoir une arborescence de fichiers qui ressemble à cela :

Arborescence projet Svelte

On va commencer par créer un composant ultra-simple qui affiche une chaine de caractère dans laquelle il y a une partie variable. On nommera ce composant DisplayName.svelte qui sera placé dans le dossier /src/lib/

// my-svelte-app/src/lib/DisplayName.svelte

<script>
  export let name;
</script>

<p id="display-name">My name is {name}</p>

name est l'élément variable ou "Props" en langage Svelte.

Pour styler notre balise <p> nous pouvons utiliser les classes utilitaires de Tailwind CSS directement dans les balises HTML de cette manière :

// my-svelte-app/src/lib/DisplayName.svelte

<script>
  export let name;
</script>

<p id="display-name" class="text-blue-600">My name is {name}</p>

La phylosophie de Tailwind CSS est d'ajouter des classes utilitaires directement dans les balises HTML alors que celle de Svelte est de diviser l'écriture du composant en trois sections : le javaScript, le HTML et les styles CSS :

// composant.svelte

<script>
  // javaScript
  export let name;
</script>

<p id="display-name" class="my-name">My name is {name}</p>

<style>
 .my-name {
   color: red;
 }
</style>

On peut se rapprocher de l'écriture Svelte tout en utilisant Tailwind CSS en faisant usage de la directive @apply qui permet d’extraire des classes utilitaires dans un composant CSS. Dans ce cas, l'écriture se rapprochera de l'écriture Svelte :

// my-svelte-app/src/lib/DisplayName.svelte

<script>
  export let name;
</script>

<p id="display-name" class="my-name">My name is {name}</p>

<style>
 .my-name {
   @apply text-red-600 hover:text-red-800;
 }
</style>

A vous de choisir la solution qui vous convient le mieux.

Le fichier main.js

Par default, les commandes d'installation que nous avons utilisé ci-dessus, implémentent le fichier main.js en supposant que vous allez créer une application complète à base de composants avec un seul composant parent monté sur une balise HTML précise :

// my-svelte-app/src/main.js 

import "./app.css";
import App from "./App.svelte";

const app = new App({
  target: document.getElementById("app"),
});

export default app;
<!-- my-svelte-app/index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Svelte + Vite App</title>
  </head>
  <body>
    <div id="app"></div> <!-- le composant parent est lié ici ... -->
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Mais nous, nous souhaitons créer des composants web individuels réutilisables et incorporables dans nos langages de templating ou nos fichiers HTML/CSS statiques. Plutôt que de monter une application entière sur une balise HTML, nous allons importer notre composant et le monter sur une balise grâce à son id. Nous allons également initialiser notre "props" avec une valeur par défaut :

// my-svelte-app/src/main.js

import DisplayName from './DisplayName.svelte';

if ( !name ) {
  name = "Default name"
} 

const displayName = new DisplayName({
  target: document.querySelector('#display-name'),
  props: {
    name: name,
  },
});

D'autres options peuvent être ajoutées sur notre instance de classe "displayName" mais je n'en ai pas l'utilité ici. Je vous laisse consulter la documentation de Svelte pour plus de détails.

Ensuite, il vous suffit de builder votre projet et d'appeler le composant dans votre template ou votre fichier HTML/CSS statique :

# Build du projet Svelte

npm run build

Appel depuis un fichier statique HTML/CSS :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Svelte + Vite App</title>
    <script type="module" crossorigin src="/assets/bundle.js"></script>
  </head>
  <body>
    <div id="display-name"></div>
    ... le reste de mon document HTML
  </body>
</html>

Le fichier bundle.js est le fichier généré par Svelte lors de la phase de build. Vous noterez également la présence d'une balise <div id="display-name"> identifiée par l'id sur lequel nous avons monté notre composant dans le fichier main.js

Avec un fichier statique HTML/CSS, la seule manière d'affecter une valeur à la "props" name est de passer par le fichier main.js

Appel depuis un template PHP :

<?php
$name = "Gilles";
?>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Svelte + Vite App</title>
    <script type="text/javascript">let name = "<?= $name ?>";</script>
    <script type="module" crossorigin src="/assets/bundle.js"></script>

  </head>
  <body>
    <div id="display-name"></div>
    ... le reste de mon document HTML
  </body>
</html>
Avec un template, je peux affecter dynamiquement une valeur à la "props" name directement dans ce template.

Vous noterez ici que je passe une valeur à ma props name en initialisant une variable javaScript name avec la valeur d'une variable PHP name

Création d'un Custom Element avec Svelte

Svelte permet également de créer un Custom Element à partir de notre composant. Dans ce cas nous pourrons appeler notre composant à l'aide d'une balise HTML personnalisée de la manière suivante : <MaBalisePerso props-name="valeur"></MaBalisePerso>.

Pour cela, nous devons ajouter dans notre composant Svelte, un élément <svelte:options> avec une directive tag dont la valeur correspondra au nom de notre élément personnalisé :

// my-svelte-app/src/lib/DisplayName.svelte

<svelte:options tag="display-name" />

<script>
  export let name;
</script>

<p id="display-name" class="my-name">My name is {name}</p>

<style>
 .my-name {
   @apply text-red-600 hover:text-red-800;
 }
</style>

Il faudra également modifier notre fichier main.js pour cette fois-ci exporter notre composant :

// my-svelte-app/src/main.js

export * from './lib/DisplayName.svelte'

Enfin, il faut indiquer au builder Vite que nous souhaitons générer un Custom Element. Cela s'implémente dans le fichier de configuration vite.config.js

// my-svelte-app/vite.config.js
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [svelte({
      compilerOptions: {
        customElement: true,
      },
    })]
})

Nous pouvons maintenant builder notre projet Svelte :

# Build du projet Svelte

npm run build

Et utiliser notre nouveau "Custom Element" <display-name> :

<?php
$name = "Gilles";
?>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Svelte + Vite App</title>
    <script type="module" crossorigin src="/assets/bundle.js"></script>

  </head>
  <body>
    <display-name name="<?= $name ?>"></display-name>
    ... le reste de mon document HTML
  </body>
</html>
J'affecte une valeur à la props name directement dans la balise personalisée.

Attention, il peut y avoir quelques inconvénients lorsque vous buildez des Custom Element depuis Svelte, ceux-ci sont indiqués dans la documentation de Svelte.