Skip to main content

Éléments personnalisés

Les composants Svelte peuvent aussi être compilés en éléments personnalisés (aussi connus sous le nom de composants web, ou web components), en utilisant l’option de compilateur customElement: true. Il est recommandé de définir un nom de balise pour le composant, et ce en utilisant l’élément <svelte:options>.

<svelte:options customElement="mon-element" />

<script>
	let { name = 'tout le monde' } = $props();
</script>

<h1>Coucou {name} !</h1>
<slot />

Vous pouvez oubliez le nom de balise pour tous les composants internes que vous ne souhaitez pas exposer, et les utiliser comme des composants normaux. Les composants consommateurs d’un composant non nommé peuvent toujours le nommer ultérieurement si nécessaire, en utilisant la propriété statique element qui contient le constructeur de l’élément personnalisé et qui est disponible lorsque l’option de compilateur customElement vaut true.

import 
type MonElement = SvelteComponent<Record<string, any>, any, any>
const MonElement: LegacyComponentType
MonElement
from './MonElement.svelte';
var customElements: CustomElementRegistry

Defines a new custom element, mapping the given name to the given constructor as an autonomous custom element.

MDN Reference

customElements
.CustomElementRegistry.define(name: string, constructor: CustomElementConstructor, options?: ElementDefinitionOptions): voiddefine('mon-element', const MonElement: LegacyComponentTypeMonElement.element);

Une fois qu’un élément personnalisé a été défini, il peut être utilisé comme un élément DOM classique :

module document
var document: Document
document
.Document.body: HTMLElement

Specifies the beginning and end of the document body.

MDN Reference

body
.InnerHTML.innerHTML: stringinnerHTML = `
<my-element> <p>Ceci est du contenu slotté</p> </my-element> `;

Toutes les props sont exposées comme propriétés de l’élément DOM (tout étant des attributs de lecture/ecriture, lorsque possible).

const 
module el
const el: Element | null
el
= var document: Documentdocument.ParentNode.querySelector<Element>(selectors: string): Element | null (+4 overloads)

Returns the first element that is a descendant of node that matches selectors.

MDN Reference

querySelector
('mon-element');
// récupère la valeur courante de la prop `name` var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without calling require('console').

Warning: The global console object’s methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
//   Error: Whoops, something bad happened
//     at [eval]:5:15
//     at Script.runInThisContext (node:vm:132:18)
//     at Object.runInThisContext (node:vm:309:38)
//     at node:internal/process/execution:77:19
//     at [eval]-wrapper:6:22
//     at evalScript (node:internal/process/execution:76:60)
//     at node:internal/main/eval_string:23:3

const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);

myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
@seesource
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100
log
(
module el
const el: Element | null
el
.name);
// définit une nouvelle valeur, qui va ainsi modifier le shadow DOM
module el
const el: Element | null
el
.name = 'les gens';

Notez que vous avez besoin de lister toutes les propriétés explicitement, en effet écrire let props = $props() sans déclarer props dans les options de composant signifie que Svelte ne peut pas connaître les props à exposer comme propriétés de l’élément DOM.

Cycle de vie du composant

Les éléments personnalisés sont créés à partir de composants Svelte avec une approche d’"emballage”. Cela signifie que le composant Svelte interne n’a pas conscience d’être un élément personnalisé. L’élément personnalisé parent — celui qui “emballe” — a la responsabilité de gérer correctement son cycle de vie.

Lorsqu’un élément personnalisé est créé, le composant Svelte qu’il emballe n’est pas créé tout de suite. Il ne sera créé que lors du premier tick suivant l’exécution de connectedCallback. Les propriétés assignées à l’élément personnalisé avant qu’il ne soit ajouté au DOM sont temporairement sauvegardées puis définies lors de la création du composant, pour que leurs valeurs ne soient pas perdues. Ceci ne fonctionne toutefois pas si vous essayez d’exécuter des fonctions exportées depuis l’élément personnalisé, elles ne seront disponibles qu’après le montage de l’élément. Si vous avez besoin d’exécuter ce genre de fonctions avant la création du composant, vous pouvez contourner le problème en utilisant l’option extend.

Lorsqu’un élément personnalisé écrit en Svelte est créé ou mis à jour, le shadow DOM reflètera la valeur lors du prochain tick, et non immédiatement. Ainsi, les mises à jour peuvent être cumulées, et les modifications de DOM — qui détachent l’élément temporairement (mais de manière synchrone) du DOM — ne découlenet pas sur le démontage du composant interne.

Le composant Svelte interne est détruit lors du premier tick suivant l’exécution de disconnectedCallback.

Options de composant

Depuis Svelte 4, lorque vous construisez un élément personnalisé, vous pouvez ajuster certains aspects en définissant un objet customElement dans <svelte:options>. Cet objet peut contenir les propriétés suivantes :

  • tag: string : une propriété tag optionnelle pour le nom de l’élément personnalisé. Si défini, un élément personnalisé portant ce nom sera défini avec le registre customElements du document lors de l’import de ce composant
  • shadow : une propriété optionnelle qui peut être définie à "none" pour renoncer à la création de la racine du Shadow DOM. Noter que les styles ne sont alors plus encapsulés, et vous ne pouvez plus utiliser de slots
  • props : une propriété optionnelle pour modifier certains détails et comportements des propriétés de votre composant. Elle offre les paramètres suivants :
    • attribute: string: Pour mettre à jour une prop d’élément personnalisé, vous avez deux alternatives : soit définir la propriété sur la référence de l’élément personnalisé comme illustré plus haut, ou utiliser un attribut HTML. Dans le deuxième cas, le nom par défaut de l’attribut est le nom de la propriété en minuscules. Vous pouvez modifier cela en assignant attribute: "<desired name>"
    • reflect: boolean : Par défaut, les mises à jour de props ne se reflètent pas sur le DOM. Pour activer ce comportement, utilisez reflect: true
    • type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object' : Lorsque la valeur d’un attribut est convertie en valeur de prop et reflétée sur le DOM, la valeur de la prop est supposée être une String par défaut. Ce n’est pas toujours le cas. Par exemple, pour un nombre, utilisez type: "Number" Vous n’avez pas besoin de lister toutes les propriétés, celles qui ne seront pas listées utiliseront les paramètres par défaut.
  • extend : une propriété optionnelle qui attend une fonction en argument. Cette fonction prend en entrée la classe de l’élément personnalisée générée par Svelte et suppose que vous renvoyiez une classe d’élément personnalisé. Ceci est utile lorsque vous avez des constraintes spécifiques sur le cycle de vie de votre élément personnalisé ou souhaitez améliorer la classe pour, par exemple, utiliser ElementInternals pour une meilleure intégration des formulaires HTML.
<svelte:options
	customElement={{
		tag: 'custom-element',
		shadow: 'none',
		props: {
			name: { reflect: true, type: 'Number', attribute: 'element-index' }
		},
		extend: (customElementConstructor) => {
			// Étend la classe pour lui permettre de participer dans les formulaires HTML
			return class extends customElementConstructor {
				static formAssociated = true;

				constructor() {
					super();
					this.attachedInternals = this.attachInternals();
				}

				// Ajoutez la fonction ici, pas plus bas dans le composant, de sorte
				// qu'elle soit toujours disponible, pas uniquement lorsque le composant
				// Svelte interne est monté
				randomIndex() {
					this.elementIndex = Math.random();
				}
			};
		}
	}}
/>

<script>
	let { elementIndex, attachedInternals } = $props();
	// ...
	function check() {
		attachedInternals.checkValidity();
	}
</script>

...

Mises en garde et limitations

Les éléments personnalisés sont un moyen utile pour empaqueter des composants et les rendre consommables dans une application n’utilisant pas Svelte, pusiqu’ils fonctionnent tout aussi bien en JS vanille qu’avec la plupart des frameworks. Il y a en revanche quelques différences importantes qu’ils important de connaître :

  • Les styles sont encapsulés, plutôt que simplement scopés (sauf si vous avez défini l’option shadow: "none"). Ceci implique que les styles non liés au composant (comme ceux que vous pourriez définir dans un fichier global.css) ne s’appliqueront pas à l’élément personnalisé. Cela vaut également pour les styles ayant le modificateur :global(...)
  • Au lieu d’être extrait dans un fichier .css distinct, les styles sont inlinés dans le composant en tant que chaîne de caractères JavaScript
  • Les éléments personnalisés ne sont en général pas adaptés pour effectuer du rendu côté serveur, puisque le shadow DOM est invisible tant que les scripts JavaScript n’ont pas été chargés
  • Avec Svelte, le contenu slotté est rendu de manière “paresseuse” (lazily). Dans le DOM classique, il est rendu immédiatement (eagerly). En d’autres termes, le contenu slotté sera créé même si l’élément <slot> du composant est dans un bloc {#if ...}. De même, inclure un <slot> dans un bloc {#each ...} ne va pas conduire à ce que le contenu slotté soit rendu plusieurs fois
  • La directive let: dépréciée n’a pas d’effet, puisque les éléments personnalisés n’ont aucun moyen de passer des données au composant parent remplissant le slot
  • Des polyfills sont nécessaires pour assurer le support de navigateurs plus anciens
  • Vous pouvez utiliser la fonctionnalité Svelte de contexte entre des composants Svelte standards au sein d’un élément personnalisé, mais vous ne pouvez pas vous servir d’un même contexte pour différents éléments personnalisés. Autrement dit, vous ne pouvez pas utiliser setContext sur un élément personnalisé parent et lire ce contexte avec getContext dans un élément personnalisé enfant
  • Ne déclarez pas de propriétés ou d’attributs commençant pas on, puisque leur usage sera interprété comme un gestionnaire d’évènement. Autrement dit, Svelte traite <custom-element oneworld={true}></custom-element> comme customElement.addEventListener('eworld', true) (et non comme customElement.oneworld = true)

Modifier cette page sur Github