Fermer

octobre 10, 2023

Extension des propriétés d’un élément HTML dans TypeScript —

Extension des propriétés d’un élément HTML dans TypeScript —


Dans ce petit conseil, extrait de Libérer la puissance de TypeScriptSteve vous montre comment étendre les propriétés d’un élément HTML dans TypeScript.

Dans la plupart des applications et des projets plus importants sur lesquels j’ai travaillé, je me retrouve souvent à créer un ensemble de composants qui sont en réalité des surensembles ou des abstractions au-dessus des éléments HTML standard. Certains exemples incluent des éléments de bouton personnalisés qui peuvent prendre un accessoire définissant si ce bouton doit être ou non un bouton principal ou secondaire, ou peut-être un élément indiquant qu’il invoquera une action dangereuse, telle que la suppression ou le retrait d’un élément de la base de données. Je souhaite toujours que mon bouton ait toutes les propriétés d’un bouton en plus des accessoires que je souhaite y ajouter.

Un autre cas courant est que je finis par créer un composant qui me permet de définir à la fois une étiquette et un champ de saisie. Je ne veux pas rajouter toutes les propriétés qu’un <input /> l’élément prend. Je veux que mon composant personnalisé se comporte comme un champ de saisie, mais aussi prenez une ficelle pour l’étiquette et câblez automatiquement le htmlFor accessoire sur le <label /> correspondre avec le id sur le <input />.

En JavaScript, je peux simplement utiliser {...props} pour transmettre tous les accessoires à un élément HTML sous-jacent. Cela peut être un peu plus délicat dans TypeScript, où je dois définir explicitement les accessoires qu’un composant acceptera. Bien qu’il soit agréable d’avoir un contrôle précis sur les types exacts acceptés par mon composant, il peut être fastidieux de devoir ajouter manuellement des informations de type pour chaque accessoire.

Dans certains scénarios, j’ai besoin d’un seul composant adaptable, comme un <div>, qui change de style en fonction du thème actuel. Par exemple, je souhaite peut-être définir les styles à utiliser selon que l’utilisateur a activé ou non manuellement le mode clair ou sombre pour l’interface utilisateur. Je ne veux pas redéfinir ce composant pour chaque élément de bloc (tel que <section>, <article>, <aside>, et ainsi de suite). Il devrait être capable de représenter différents éléments HTML sémantiques, TypeScript s’adaptant automatiquement à ces changements.

Il existe quelques stratégies que nous pouvons utiliser :

  • Pour les composants pour lesquels nous créons une abstraction sur un seul type d’élément, nous pouvons étendre les propriétés de cet élément.
  • Pour les composants où l’on souhaite définir différents éléments, nous pouvons créer des composants polymorphes. UN composant polymorphe est un composant conçu pour s’afficher sous forme de différents éléments ou composants HTML tout en conservant les mêmes propriétés et comportements. Il nous permet de spécifier un accessoire pour déterminer son type d’élément rendu. Les composants polymorphes offrent flexibilité et réutilisabilité sans que nous ayons à réimplémenter le composant. Pour un exemple concret, vous pouvez regarder Implémentation par Radix d’un composant polymorphe.

Dans ce didacticiel, nous examinerons la première stratégie.

Mise en miroir et extension des propriétés d’un élément HTML

Commençons par ce premier exemple mentionné dans l’introduction. Nous souhaitons créer un bouton intégré avec le style approprié pour une utilisation dans notre application. En JavaScript, nous pourrions faire quelque chose comme ceci :

const Button = (props) => {
  return <button className="button" {...props} />;
};

Dans TypeScript, nous pourrions simplement ajouter ce dont nous savons avoir besoin. Par exemple, nous savons que nous avons besoin du children si nous voulons que notre bouton personnalisé se comporte de la même manière qu’un bouton HTML :

const Button = ({ children }: React.PropsWithChildren) => {
  return <button className="button">{children}</button>;
};

Vous pouvez imaginer qu’ajouter des propriétés une par une pourrait devenir un peu fastidieux. Au lieu de cela, nous pouvons dire à TypeScript que nous voulons faire correspondre les mêmes accessoires qu’il utiliserait pour un <button> élément dans React :

const Button = (props: React.ComponentProps<'button'>) => {
  return <button className="button" {...props} />;
};

Mais nous avons un nouveau problème. Ou plutôt, nous avait un problème qui aussi existait dans l’exemple JavaScript et que nous avons ignoré. Si quelqu’un utilise notre nouveau Button le composant passe dans un className prop, cela remplacera notre className. Nous pourrions (et nous le ferons) ajouter du code pour résoudre ce problème dans un instant, mais je ne veux pas laisser passer l’occasion de vous montrer comment utiliser un type d’utilitaire dans TypeScript pour dire « Je veux utiliser tous des accessoires d’un bouton HTML sauf pour un (ou plusieurs) » :

type ButtonProps = Omit<React.ComponentProps<'button'>, 'className'>;

const Button = (props: ButtonProps) => {
  return <button className="button" {...props} />;
};

Désormais, TypeScript nous empêchera, ou quiconque, de transmettre un className propriété dans notre Button composant. Si nous voulions simplement étendre la liste des classes avec tout ce qui est transmis, nous pourrions le faire de différentes manières. Nous pourrions simplement l’ajouter à la liste :

type ButtonProps = React.ComponentProps<'button'>;

const Button = (props: ButtonProps) => {
  const className="button " + props.className;

  return <button className={className.trim()} {...props} />;
};

J’aime utiliser le clsx bibliothèque lorsque vous travaillez avec des classes, car elle s’occupe de la plupart de ces types de choses en notre nom :

import React from 'react';
import clsx from 'clsx';

type ButtonProps = React.ComponentProps<'button'>;

const Button = ({ className, ...props }: ButtonProps) => {
  return <button className={clsx('button', className)} {...props} />;
};

export default Button;

Nous avons appris à limiter les accessoires qu’un composant acceptera. Pour étendre les accessoires, nous pouvons utiliser un intersection:

type ButtonProps = React.ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary';
};

Nous disons maintenant que Button accepte tous les accessoires qu’un <button> l’élément accepte plus un de plus : variant. Cet accessoire apparaîtra avec tous les autres accessoires dont nous avons hérité HTMLButtonElement.

La variante apparaît comme accessoire sur notre composant Button

Nous pouvons ajouter un support à notre Button pour ajouter également cette classe :

const Button = ({ variant, className, ...props }: ButtonProps) => {
  return (
    <button
      className={clsx(
        'button',
        variant === 'primary' && 'button-primary',
        variant === 'secondary' && 'button-secondary',
        className,
      )}
      {...props}
    />
  );
};

Nous pouvons maintenant mettre à jour src/application.tsx pour utiliser notre nouveau composant bouton :

diff --git a/src/application.tsx b/src/application.tsx
index 978a61d..fc8a416 100644
--- a/src/application.tsx
+++ b/src/application.tsx
@@ -1,3 +1,4 @@
+import Button from './components/button';
 import useCount from './use-count';

 const Counter = () => {
@@ -8,15 +9,11 @@ const Counter = () => {
       <h1>Counter</h1>
       <p className="text-7xl">{count}</p>
       <div className="flex place-content-between w-full">
-        <button className="button" onClick={decrement}>
+        <Button onClick={decrement}>
           Decrement
-        </button>
-        <button className="button" onClick={reset}>
-          Reset
-        </button>
-        <button className="button" onClick={increment}>
-          Increment
-        </button>
+        </Button>
+        <Button onClick={reset}>Reset</Button>
+        <Button onClick={increment}>Increment</Button>
       </div>
       <div>
         <form
@@ -32,9 +29,9 @@ const Counter = () => {
         >
           <label htmlFor="set-count">Set Count</label>
           <input type="number" id="set-count" name="set-count" />
-          <button className="button-primary" type="submit">
+          <Button variant="primary" type="submit">
             Set
-          </button>
+          </Button>
         </form>
       </div>
     </main>

Vous pouvez trouver les changements ci-dessus dans le button branche du dépôt GitHub pour ce tutoriel.

Création de composants composites

Un autre composant courant que je finis généralement par fabriquer moi-même est un composant qui connecte correctement une étiquette et un élément d’entrée avec le bon for et id attributs respectivement. J’ai tendance à me lasser de taper ceci encore et encore :

<label htmlFor="set-count">Set Count</label>
<input type="number" id="set-count" name="set-count" />

Sans étendre les accessoires d’un élément HTML, je pourrais finir par ajouter lentement des accessoires si nécessaire :

type LabeledInputProps = {
  id?: string;
  label: string;
  value: string | number;
  type?: string;
  className?: string;
  onChange?: ChangeEventHandler<HTMLInputElement>;
};

Comme nous l’avons vu avec le bouton, nous pouvons le refactoriser de la même manière :

type LabeledInputProps = React.ComponentProps<'input'> & {
  label: string;
};

Autre que label, que nous transmettons à l’étiquette (euhh) que nous souhaitons souvent regrouper avec nos entrées, nous transmettons manuellement les accessoires un par un. Voulons-nous ajouter autofocus? Mieux vaut ajouter un autre accessoire. Il vaudrait mieux faire quelque chose comme ceci :

import { ComponentProps } from 'react';

type LabeledInputProps = ComponentProps<'input'> & {
  label: string;
};

const LabeledInput = ({ id, label, ...props }: LabeledInputProps) => {
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input {...props} id={id} readOnly={!props.onChange} />
    </>
  );
};

export default LabeledInput;

Nous pouvons échanger notre nouveau composant dans src/application.tsx:

<LabeledInput
  id="set-count"
  label="Set Count"
  type="number"
  onChange={(e) => setValue(e.target.valueAsNumber)}
  value={value}
/>

Nous pouvons extraire les éléments avec lesquels nous devons travailler et ensuite transmettre tout le reste au <input /> composant, puis prétendons pour le reste de nos jours que c’est un standard HTMLInputElement.

TypeScript s’en fiche, puisque HTMLElement est assez flexible, car le DOM est antérieur à TypeScript. Il ne se plaint que si nous y ajoutons quelque chose de complètement flagrant.

Vous pouvez voir tous les changements ci-dessus dans le input branche du dépôt GitHub pour ce tutoriel.

Cet article est extrait de Libérer la puissance de TypeScriptdisponible sur SitePoint Premium et chez les détaillants de livres électroniques.






Source link