Cours Svelte et SvelteKit · Module 2: Composants

Organisation du projet

4 min de lecture ·
Dans cette leçon4

Un projet bien organisé, c'est comme un village viking bien organisé : tout le monde sait où se trouve chaque chose !

Structure recommandée

src/
├── lib/                      # Code partagé (alias #lib)
│   ├── components/           # Composants d'interface
│   │   ├── ui/              # De base (Button, Input)
│   │   │   ├── Button.svelte
│   │   │   └── Input.svelte
│   │   ├── layout/          # Mises en page (Header, Footer)
│   │   │   ├── Header.svelte
│   │   │   └── Footer.svelte
│   │   └── features/        # Fonctionnels
│   │       ├── WarriorCard.svelte
│   │       └── ShipList.svelte
│   ├── state/               # État partagé (runes dans des fichiers .svelte.js)
│   │   ├── warriors.svelte.js
│   │   └── ships.svelte.js
│   ├── utils/               # Fonctions utilitaires
│   │   ├── formatters.js
│   │   └── validators.js
│   └── types/               # Types TypeScript
│       └── index.ts
├── routes/                   # Pages (SvelteKit)
│   ├── +page.svelte
│   ├── +layout.svelte
│   └── warriors/
│       └── +page.svelte
└── app.html                  # Template HTML

Le répertoire state/ contient l'état partagé par plusieurs composants. En Svelte 5, ce sont de simples fichiers .svelte.js dans lesquels les runes comme $state fonctionnent. Les projets plus anciens ont ici un répertoire stores/ avec des stores Svelte : tu les découvriras au Puits de Mimir (module 8).

Alias d'import

Un projet créé avec npx sv create possède l'alias #lib dans package.json, qui pointe vers le répertoire src/lib. C'est une fonctionnalité de Node.js appelée subpath imports (le champ imports) :

{
  "imports": {
    "#lib": "./src/lib/index.js",
    "#lib/*": "./src/lib/*"
  }
}

Grâce à lui, plus besoin de compter les ../ dans les chemins. N'oublie simplement pas l'extension du fichier à la fin de l'import (.svelte, .js) :

<script>
  // Au lieu de : import Button from '../../../lib/components/ui/Button.svelte'
  import Button from '#lib/components/ui/Button.svelte';

  // État partagé
  import { army } from '#lib/state/warriors.svelte.js';

  // Utilitaires
  import { formatDate } from '#lib/utils/formatters.js';
</script>

Dans SvelteKit 2, ce même alias s'appelait $lib et fonctionnait sans extensions : tu le verras dans du code plus ancien. SvelteKit 3 ne le crée plus.

Conventions de nommage

Fichiers de composants

  • PascalCase : WarriorCard.svelte, ShipList.svelte
  • Le nom = ce que le composant représente

Fichiers utilitaires

  • camelCase ou kebab-case : formatters.js, api-client.ts

Répertoires

  • kebab-case : viking-camp/, ui-components/

Exemple : Composant avec structure complète

Ce composant réunit tout au même endroit. Le parent lui passe un guerrier et une fonction onselect, que le composant appelle quand quelqu'un choisit la carte. C'est une callback prop : en Svelte 5, les composants parlent à leur parent grâce à des fonctions dans les props (plus de détails à Asgard, module 4). Dans un projet TypeScript, tu importes aussi les fichiers .ts avec l'extension .js : TypeScript trouve tout seul le fichier index.ts.

<!-- src/lib/components/features/WarriorCard.svelte -->
<script lang="ts">
  // Imports
  import Avatar from '#lib/components/ui/Avatar.svelte';
  import { formatHealth } from '#lib/utils/formatters.js';
  import type { Warrior } from '#lib/types/index.js';

  // Props - données et callback du parent
  interface Props {
    warrior: Warrior;
    selected?: boolean;
    onselect?: (id: number) => void;
  }

  let { warrior, selected = false, onselect }: Props = $props();

  // State
  let showDetails = $state(false);

  // Derived
  let healthStatus = $derived(warrior.health > 50 ? 'healthy' : 'wounded');
</script>

<article
  class="warrior-card"
  class:selected
  class:wounded={healthStatus === 'wounded'}
>
  <Avatar src={warrior.avatar} alt={warrior.name} />
  <h3>{warrior.name}</h3>
  <p>Santé : {formatHealth(warrior.health)}</p>

  {#if showDetails}
    <div class="details">
      <p>Arme : {warrior.weapon}</p>
      <p>Rang : {warrior.rank}</p>
    </div>
  {/if}

  <button onclick={() => onselect?.(warrior.id)}>Choisir</button>
  <button onclick={() => showDetails = !showDetails}>
    {showDetails ? 'Masquer' : 'Détails'}
  </button>
</article>

<style>
  .warrior-card {
    padding: 1rem;
    border: 2px solid #444;
    border-radius: 8px;
    transition: all 0.2s;
  }

  .warrior-card:hover {
    border-color: #d4af37;
  }

  .warrior-card.selected {
    background: rgba(212, 175, 55, 0.1);
    border-color: #d4af37;
  }

  .warrior-card.wounded {
    border-color: #8b0000;
  }
</style>

interface Props décrit ce que le composant accepte : un guerrier obligatoire, un indicateur selected facultatif et une fonction onselect facultative. onselect?.(warrior.id) n'appelle la fonction que si le parent l'a passée. En Svelte 4, on utilisait createEventDispatcher au lieu des callback props : en Svelte 5, c'est déprécié.

Entrées de la documentation utilisées dans cette leçon

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Où, dans un projet SvelteKit, doivent se trouver les composants UI partagés ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Clique sur les conventions de nommage dans l'ordre : composants, fichiers utilitaires, répertoires

Articles utiles