Champs de signature

Développement

Amélioration de la signature de documents : présentation de 5 nouveaux champs avancés

Amélioration de la signature de documents : présentation de 5 nouveaux champs avancés

9 août 2024

Jusque récemment, Documenso proposait un ensemble de 5 champs pour la signature de documents : signature, email, nom, date, et un champ de texte pour des informations supplémentaires. Bien que ces champs couvraient les exigences de base pour la signature de documents, nous avons reconnu le besoin d'une plus grande flexibilité et variété.

En conséquence, nous avons décidé d'introduire plusieurs champs supplémentaires, tels que :

  • (une version améliorée) Champ de texte

  • Champ numérique

  • Champ radio

  • Champ de case à cocher

  • Champ déroulant/Sélection

Ces nouveaux champs apportent plus de flexibilité et de variété à Documenso. En tant que propriétaire du document, ils vous permettent de rassembler des informations plus spécifiques ou supplémentaires de la part des signataires.

Introduction de Nouveaux Champs

Examinons de plus près chaque type de nouveau champ.

Champ de Texte

Bien que le champ de texte ait déjà été disponible, il ne pouvait pas être configuré. C'était une simple boîte de saisie où les signataires pouvaient entrer une seule ligne de texte.

L'image illustre l'ancien champ de texte dans l'éditeur de documents.


Le champ de texte rénové offre maintenant une gamme d'options de configuration, vous permettant de :

  • Ajouter une étiquette, un texte d'espace réservé, du texte par défaut, et une limite de caractères

  • Définir le champ comme requis ou en lecture seule

Du côté de la signature, le champ est resté visuellement presque le même. La seule chose qui a changé est la fonctionnalité, qui doit prendre en compte les règles de validation. Par exemple, si le champ est requis, le signataire doit entrer une valeur pour le signer. Ou, si le champ a une limite de caractères, la valeur entrée par le signataire ne doit pas dépasser cette limite.

L'image ci-dessous illustre quatre champs de texte différents avec diverses configurations.

Le premier champ de texte n'a pas de valeur par défaut ("Ajouter du texte") ou de configuration. Vous pouvez signer le champ en entrant n'importe quel texte.

Le deuxième champ de texte, "label-1"/"text-1", a les configurations suivantes :

  • Étiquette

  • Texte d'espace réservé

  • Texte par défaut

  • Limite de caractères

Étant donné qu'il y a une valeur par défaut, le champ se signe automatiquement avec cette valeur. Cependant, vous pouvez resigné le champ avec une nouvelle valeur qui ne dépasse pas la limite de caractères.

Le troisième champ, "label-2"/"text-2", a les mêmes configurations que le deuxième, avec une addition - l'option requise est cochée. Lorsque le champ est marqué comme requise, vous devez le signer avant de compléter le document.

En dehors de cela, il fonctionne comme le deuxième champ.

Le quatrième champ, "label-3"/"text-3", a les mêmes configurations que le deuxième, avec une addition — lecture seule est cochée. Cela signifie que le champ se signe automatiquement avec la valeur par défaut, et vous ne pouvez pas le modifier.

Champs Non Signés

Vous pouvez annuler la signature d'un champ pour changer la valeur et le signer à nouveau. L'état non signé du champ varie en fonction de sa configuration :

  • Si le champ a une étiquette, celle-ci s'affiche au lieu de "Ajouter du texte" lorsqu'il est non signé.

  • Si le champ a une valeur par défaut, la valeur par défaut sera affichée lorsqu'il est non signé.

  • Si le champ a à la fois une étiquette et une valeur par défaut, l'étiquette prendra le pas et sera affichée lorsqu'il est non signé.

L'image ci-dessous montre l'état non signé des champs de texte.

L'unique exception est le quatrième champ, en lecture seule, qui ne peut pas être non signé ou modifié.

Champ Numérique

Nous avons également introduit un nouveau "Champ Numérique" pour insérer et signer des documents avec des valeurs numériques. Ce champ aide à collecter des quantités, des mesures, et d'autres données mieux représentées sous forme de nombres.

Le "Champ Numérique" offre une gamme d'options de configuration, ce qui vous permet de :

  • Définir une étiquette, un texte d'espace réservé et une valeur par défaut

  • Spécifier le format du nombre

  • Marquer le champ comme requis ou lecture seule

  • Spécifier les valeurs minimales et maximales

Le champ Numérique ressemble et fonctionne de manière similaire au Champ de Texte. La différence est qu'il n'accepte que des valeurs numériques et a 2 configurations supplémentaires : le format du nombre et les valeurs minimales et maximales.

Champ Radio

Les boutons radio permettent aux signataires de sélectionner une seule option d'une liste prédéfinie que le propriétaire du document définit.

Avant d'envoyer le document pour signature, vous devez ajouter au moins une option radio, qui peut contenir une chaîne ou une valeur vide et peut être cochée ou décochée. Cependant, il est important de noter qu'une seule option peut être cochée à la fois.

En ce qui concerne la configuration du champ, vous pouvez marquer le champ comme requis ou lecture seule.

L'image ci-dessous montre ce que voit le signataire après que le document a été envoyé pour signature.

Remarque : L'image est modifiée pour afficher à la fois les états non signés et signés du champ.

Étant donné que le champ a une option présélectionnée (option radio-val-2-checked), il signera automatiquement avec cette valeur et apparaîtra comme le champ marqué avec le numéro 1.

Si le champ n'est pas en lecture seule, le signataire peut :

  • Annuler la signature du champ et choisir une autre option en cliquant dessus.

  • Resigner avec la valeur par défaut en rafraîchissant la page lorsque le champ est non signé.

Cependant, si le champ est marqué comme étant en lecture seule, le signataire ne peut pas modifier la valeur présélectionnée.

Champ Déroulant/Sélection

Nous avons également introduit un nouveau champ "Déroulant/Sélection" qui permet aux signataires de choisir une option parmi une liste prédéfinie de choix. Ce type de champ est idéal dans des scénarios avec des options valides limitées, comme sélectionner un pays, un état ou une catégorie.

Lors de la configuration d'un champ "Déroulant/Sélection", vous pouvez :

  • Ajouter plusieurs options

  • Marquer le champ comme requis ou lecture seule

  • Choisir une option par défaut parmi la liste de choix

Sur la page de signature, le champ "Déroulant/Sélection" apparaît comme indiqué ci-dessous :

Voici comment le champ "Déroulant/Sélection" fonctionne :

  • Si aucune valeur par défaut n'est définie, le champ ne se signe pas automatiquement. Le signataire doit cliquer sur le champ et choisir une option dans la liste déroulante pour le signer.

  • Après signature, le champ affiche la valeur sélectionnée, similaire à un champ de texte signé.

  • Si le champ est marqué comme requis, les signataires doivent sélectionner une valeur avant de compléter le processus de signature.

  • Si le champ est marqué comme lecture seule, les signataires peuvent voir la valeur sélectionnée mais ne peuvent pas la modifier.

Champ de Case à Cocher

Le dernier champ introduit est le champ "Case à Cocher", qui permet aux signataires de sélectionner plusieurs options dans une liste prédéfinie. Ce champ est utile dans des scénarios où les signataires doivent choisir plusieurs éléments ou accepter plusieurs conditions, par exemple.

Avant d'envoyer le document pour signature, vous devez ajouter au moins une option de case à cocher. Cette option peut contenir une chaîne ou une valeur vide et peut être cochée ou décochée. Contrairement au champ "Radio", le champ "Case à Cocher" peut avoir plusieurs options cochées.

Comme les autres champs, vous pouvez marquer le champ "Case à Cocher" comme requis ou lecture seule. De plus, il a également un champ de validation, et vous pouvez spécifier combien de cases à cocher le signataire doit signer :

  • Sélectionner au moins X (un nombre de 1 à 10)

  • Sélectionner au maximum X (un nombre de 1 à 10)

  • Sélectionner exactement X (un nombre de 1 à 10)

Lorsque le signataire reçoit le document, il verra le champ "Case à Cocher" comme montré ci-dessous :

L'image illustre les deux états du champ - signé et non signé. Dans cet exemple, le champ 'Case à Cocher' a deux options cochées par défaut, donc il se signe automatiquement.

Le champ marqué '1' apparaît lorsque le signataire visite la page pour la première fois ou lorsque l'utilisateur rafraîchit la page et qu'aucune option n'est sélectionnée. Le champ marqué '2' affiche l'état vidé, où toutes les sélections ont été désélectionnées. Cela montre à quoi ressemble le champ lorsque l'utilisateur efface toutes les sélections.

Dans cet exemple, aucune règle de validation n'a été définie, permettant au signataire de sélectionner n'importe quelle option. Cependant, lorsqu'une règle de validation est appliquée, les signataires doivent respecter les critères spécifiés pour compléter le processus de signature.

Défis de Développement

L'introduction de ces nouveaux champs n'a pas été sans ses défis. Les principaux défis étaient :

  • Déterminer comment stocker les nouvelles informations pour les champs dans la base de données

  • Differenciation des destinataires utilisant des couleurs

  • Stocker les paramètres avancés pour les champs locaux sur le frontend

  • Implémentation des champs de Case à Cocher et Radio

1er Défi : Stocker les Informations des Nouveaux Champs

Le premier défi a été de décider comment stocker les informations supplémentaires pour chaque nouveau champ dans la base de données. Chaque champ a des propriétés uniques, seules requises et lecture seule étant partagées par tous les champs avancés.

Le modèle existant Field dans la base de données ressemble à ceci :

model Field {
 id          Int        @id @default(autoincrement())
 secondaryId String     @unique @default(cuid())
 documentId  Int?
 templateId  Int?
 recipientId Int
 type        FieldType
 page        Int
 positionX   Decimal    @default(0)
 positionY   Decimal    @default(0)
 width       Decimal    @default(-1)
 height      Decimal    @default(-1)
 customText  String
 inserted    Boolean
 Document    Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
 Template    Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
 Recipient   Recipient  @relation(fields: [recipientId], references: [id], onDelete: Cascade)
 Signature   Signature?

 @@index([documentId])
 @@index([templateId])
 @@index([recipientId])
}

Au départ, nous avions envisagé de créer une nouvelle table FieldMeta avec des colonnes pour chaque propriété de champ. Cependant, cette approche présente 2 problèmes.

Tout d'abord, les champs avancés ne partagent que deux propriétés communes : requises et lecture seule. Étant donné que toutes les autres propriétés sont uniques à chaque type de champ, cela entraînerait de nombreuses colonnes nullables dans le modèle FieldMeta.

Deuxièmement, la création d'une nouvelle table de base de données avec des colonnes pour chaque propriété de champ et les relations associées augmenterait la complexité de la base de données.

En conséquence, nous avons décidé de chercher une autre solution qui fonctionnerait mieux avec notre cas d'utilisation.

Solution : Champ JSONB

Étant donné que les données des paramètres avancés sont uniques à chaque champ, nous avons décidé de les stocker sous forme de JSON en utilisant le type de données JSONB de PostgreSQL. Nous avons ajouté une nouvelle propriété optionnelle fieldMeta de type JSONB au modèle Field :

model Field {
 id          Int        @id @default(autoincrement())
 secondaryId String     @unique @default(cuid())
 documentId  Int?
 templateId  Int?
 recipientId Int
 type        FieldType
 page        Int
 positionX   Decimal    @default(0)
 positionY   Decimal    @default(0)
 width       Decimal    @default(-1)
 height      Decimal    @default(-1)
 customText  String
 inserted    Boolean
 Document    Document?  @relation(fields: [documentId], references: [id], onDelete: Cascade)
 Template    Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
 Recipient   Recipient  @relation(fields: [recipientId], references: [id], onDelete: Cascade)
 Signature   Signature?
 fieldMeta   Json? <<<<<

Cette approche nous permet de stocker les paramètres de chaque champ sous forme d'objet JSON. Nous utilisons des schémas Zod pour analyser et valider les métadonnées du champ lors de la lecture ou de l'écriture dans la base de données pour garantir l'intégrité des données.

Cette approche a plusieurs avantages :

  • Consistance : L'application utilise le même schéma Zod pour récupérer et insérer des données dans la base de données. Cela signifie que les données sont cohérentes à travers l'application.

  • Sécurité des types : En analysant les données avec Zod, nous pouvons garantir que les données correspondent aux types et à la structure attendus. Nous pouvons également utiliser l'utilitaire infer de Zod pour permettre une forte typage et une autocomplétion.

  • Meilleure gestion des erreurs : Zod fournit des messages d'erreur détaillés indiquant quelle partie des données est invalide. Cela rend le débogage et la résolution de problèmes plus faciles et plus rapides.

  • Maintenabilité : La réutilisation du même schéma Zod pour récupérer et insérer des données dans la base de données rend la structure des données plus facile à maintenir.

Cependant, l'utilisation de JSONB a également des inconvénients, comme la consultation des données. Étant donné que les données sont stockées sous forme de JSON (plus précisément, au format binaire), les requêtes complexes peuvent être moins efficaces comparées à la consultation de données relationnelles normalisées. De plus, la consultation des données nécessite des opérateurs et des fonctions spécifiques, tels que ->, ->>, @>, et ?. Cela rend les requêtes plus verbeuses et moins intuitives, et par conséquent, elles nécessitent plus de précision.

Un autre inconvénient est la surcharge de stockage. Les données JSONB sont stockées dans un format binaire, ce qui peut entraîner une certaine surcharge de stockage par rapport à des données relationnelles normalisées. Dans les cas où les données JSON sont volumineuses ou contiennent beaucoup d'informations redondantes, la surcharge de stockage peut être significative.

Malgré ces inconvénients, le type JSONB convient à notre cas d'utilisation, car les informations meta de champ sont relativement petites et ne nécessitent pas de requêtes complexes. La flexibilité de JSONB correspond à la nature dynamique du champ Meta.

Postgres fournit 2 champs pour stocker des données JSON - json et jsonb. Pour plus d'informations, vous pouvez consulter la documentation.

2ème Défi : Stockage des Paramètres Avancés des Champs sur le Frontend

Le défi suivant était de trouver le meilleur moyen de stocker les paramètres avancés des champs saisis par les utilisateurs.

Actuellement, l'application ne sauvegarde que les champs et les paramètres associés dans la base de données lorsque l'utilisateur passe à l'étape suivante.

Les champs sont stockés localement jusqu'à ce que l'utilisateur passe à l'étape suivante. Cela signifie que tous les champs et leurs paramètres sont perdus lorsque l'utilisateur :

  • Ferme l'onglet des paramètres avancés

  • Rafraîchit la page

  • Ferme l'onglet

  • Navigue vers l'étape précédente

À l'avenir, nous prévoyons d'améliorer ce flux et de sauvegarder les champs au flou, préservant ainsi les données de l'utilisateur même s'il navigue ailleurs. Cependant, jusqu'à ce moment-là, nous avions besoin d'une solution pour sauvegarder les paramètres avancés lorsque l'utilisateur ferme l'onglet de paramètres.

Solution : Stockage Local

Notre solution temporaire consiste à stocker les paramètres avancés dans le stockage local, car les champs ne sont disponibles que localement. Si les champs avaient été sauvegardés dans la base de données, nous aurions pu stocker les paramètres avancés à côté d'eux.

Comme les champs ne sont pas sauvegardés dans la base de données, nous devons persister les données jusqu'à ce que l'utilisateur passe à l'étape suivante, à quel point les données sont sauvegardées dans la base de données. Stocker les données dans le stockage local permet aux utilisateurs d'ouvrir, fermer et configurer divers champs dans l'onglet de paramètres avancés sans perdre d'informations.

Lorsque l'utilisateur passe à l'étape suivante, les champs et leurs paramètres avancés sont sauvegardés dans la base de données, et le stockage local est effacé.

Nous avons également reconnu les dangers de sauvegarder des données dans le stockage local, car les utilisateurs pourraient les modifier et casser l'application. En conséquence, nous avons mis en œuvre de vastes vérifications tant sur le backend que sur le frontend, en plus d'analyser et de valider les données avec Zod.

Cependant, cette solution a des limites. Les données sont toujours perdues lorsque l'utilisateur :

  • Rafraîchit la page

  • Navigue vers l'étape précédente

  • Ferme le navigateur

Dans ces cas, les champs sont supprimés du document. Une amélioration future pour sauvegarder les champs dans la base de données au flou résoudra ce problème.

3ème Défi : Champs Radio et Case à Cocher

L'implémentation des champs Radio et Case à Cocher a été difficile tant d'un point de vue logique que design. Les deux champs peuvent contenir des valeurs vides et non vides, et le champ Case à Cocher permet aux utilisateurs de sélectionner plusieurs valeurs vides/non vides.

L'image ci-dessus montre les champs Radio et Case à Cocher dans l'éditeur de documents. Le champ Radio à gauche a 4 options, dont 1 est cochée. Le champ Case à Cocher à droite a 4 options, dont 2 sont cochées.

Le champ Radio était plus facile à implémenter car les utilisateurs ne peuvent sélectionner qu'une seule option, ce qui entraîne une logique plus simple. Le signataire clique sur une option pour la choisir, et le champ se signe automatiquement avec cette valeur. Pour changer la sélection, l'utilisateur clique sur une autre option, annulant la signature du champ et le resignant avec la nouvelle valeur.

Le champ Case à Cocher était plus difficile car :

  • Les signataires peuvent sélectionner plusieurs options simultanément, ce qui entraîne que le champ contient plusieurs valeurs.

  • Il peut avoir des règles de validation (par exemple, sélectionner au moins, au maximum, ou exactement X options).

  • Les utilisateurs peuvent cocher/décocher les options en cliquant dessus ou vider le champ avec un bouton.

Ces facteurs rendent le champ Case à Cocher plus complexe et plus difficile à implémenter correctement.

Solution

Au lieu de se concentrer sur une solution spécifique, nous discuterons de l'implémentation générale et de ses aspects les plus difficiles. Je fournirai un lien vers l'implémentation complète pour chaque champ afin que vous puissiez la consulter.

Champ Radio

Le fonctionnement de la signature pour le champ Radio consiste à extraire les données de la base de données et à afficher les options disponibles. Si le champ a une valeur par défaut définie par l'expéditeur du document, il se signe automatiquement avec cette valeur.

...
  const values = parsedFieldMeta.values?.map((item) => ({
    ...item,
 value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
 }));
...
  const shouldAutoSignField =
 (!field.inserted && selectedOption) ||
 (!field.inserted && defaultValue) ||
 (!field.inserted && parsedFieldMeta.readOnly && defaultValue);
...

  useEffect(() => {
    if (shouldAutoSignField) {
      void executeActionAuthProcedure({
        onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
 actionTarget: field.type,
 });
 }
 }, [selectedOption, field]);

Vous pouvez voir l'implémentation complète du champ radio dans le fichier radio-field.tsx.

Si le champ n'est pas en lecture seule et que l'utilisateur clique sur une autre option, le champ annule la signature et se resignera avec la nouvelle valeur. Les champs en lecture seule ne peuvent pas être modifiés.

La valeur est sauvegardée dans la base de données chaque fois que le champ est signé, que ce soit par auto-signature ou par l'utilisateur. De même, la valeur est supprimée de la base de données lorsque le champ est non signé.

Étant donné que le champ Radio peut contenir des valeurs vides, nous parcourons les valeurs et remplaçons les vides par une chaîne unique empty-value-${item.id}. Cela est dû au fait que la chaîne vide n'est pas une valeur valide pour le champ, et nous devons différencier entre les valeurs vides et non vides.

Champ Case à Cocher

L'implémentation du champ Case à Cocher est similaire à celle du champ Radio, avec les principales différences suivantes :

  • Les champs Case à Cocher peuvent contenir plusieurs valeurs.

  • Les champs Case à Cocher ont des règles de validation qui doivent être appliquées.

...
  const values = parsedFieldMeta.values?.map((item) => ({
    ...item,
 value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
 }));

  const [checkedValues, setCheckedValues] = useState(
    values
      ?.map((item) =>
        item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
 )
 .filter(Boolean) || [],
 );
...
...
  const values = parsedFieldMeta.values?.map((item) => ({
    ...item,
 value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
 }));

  const [checkedValues, setCheckedValues] = useState(
    values
      ?.map((item) =>
        item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
 )
 .filter(Boolean) || [],
 );

  const checkboxValidationRule = parsedFieldMeta.validationRule;
  const checkboxValidationLength = parsedFieldMeta.validationLength;
  const validationSign = checkboxValidationSigns.find(
 (sign) => sign.label === checkboxValidationRule,
 );
...

Ensuite, nous récupérons la règle de validation et la longueur de la base de données et trouvons le signe de validation correspondant (par exemple, ">=", "=", "\<=") en fonction de l'étiquette de la règle. Le tableau checkboxValidationSigns associe les étiquettes de règles à leurs signes correspondants.

export const checkboxValidationSigns = [
  {
    label: 'Select at least',
    value: '>=',
  },
  {
    label: 'Select exactly',
    value: '=',
  },
  {
    label: 'Select at most',
    value: '<=',
  },
];

Ensuite, nous vérifions si la condition de longueur est remplie en fonction de la règle de validation, du signe et de la longueur. Si elle est remplie, l'utilisateur peut procéder à la signature du champ. Sinon, il doit sélectionner le nombre correct d'options.

...
  const values = parsedFieldMeta.values?.map((item) => ({
    ...item,
 value: item.value.length > 0 ? item.value : `empty-value-${item.id}`,
 }));

  const [checkedValues, setCheckedValues] = useState(
    values
      ?.map((item) =>
        item.checked ? (item.value.length > 0 ? item.value : `empty-value-${item.id}`) : '',
 )
 .filter(Boolean) || [],
 );

  const checkboxValidationRule = parsedFieldMeta.validationRule;
  const checkboxValidationLength = parsedFieldMeta.validationLength;
  const validationSign = checkboxValidationSigns.find(
 (sign) => sign.label === checkboxValidationRule,
 );

  const isLengthConditionMet = useMemo(() => {
    if (!validationSign) return true;
    return (
 (validationSign.value === '>=' && checkedValues.length >= (checkboxValidationLength || 0)) ||
 (validationSign.value === '=' && checkedValues.length === (checkboxValidationLength || 0)) ||
 (validationSign.value === '<=' && checkedValues.length <= (checkboxValidationLength || 0))
 );
 }, [checkedValues, validationSign, checkboxValidationLength]);
...

En résumé, le champ Case à Cocher permet aux signataires de sélectionner plusieurs options, le champ signant automatiquement en fonction de ces sélections. Les signataires peuvent annuler la signature du champ en désélectionnant les options ou en effaçant toutes les sélections. Le système applique les règles de validation tout au long de ce processus, garantissant que les signataires sélectionnent le nombre requis d'options pour signer avec succès le champ.

Vous pouvez voir l'implémentation complète du champ Case à Cocher dans le fichier checkbox-field.tsx.

4ème Défi : Couleurs des Destinataires

Un autre défi que nous avons rencontré était d'utiliser des couleurs pour différencier les destinataires. Nous avions besoin de générer et de réutiliser dynamiquement les mêmes classes Tailwind à travers plusieurs composants. Cependant, TailwindCSS n'inclut que les classes CSS utilisées dans le projet, supprimant celles non utilisées de la construction finale. Cela a entraîné des couleurs qui n'étaient pas appliquées aux composants, car les classes n'étaient pas utilisées dans le code.

Les images ci-dessous illustrent les couleurs des destinataires dans 2 états différents.

Dans la première image, le champ "Signature" à droite est dans l'état actif (bleu), déclenché lorsque l'utilisateur clique sur le champ pour le faire glisser sur le document. Le champ de signature à gauche, placé sur le document, est dans l'état normal.

La première image illustre le champ "Signature" dans l'état actif, déclenché lorsque l'utilisateur clique dessus.

La deuxième image montre le champ "Signature" dans l'état normal.

L'éditeur de documents est composé de divers composants (champs, destinataires, etc.), ce qui signifie que les mêmes couleurs et le même code sont réutilisés à travers plusieurs composants.

export const combinedStyles = {
  'orange-500': {
 ringColor: 'ring-orange-500/30 ring-offset-orange-500',
 borderWithHover: 'border-orange-500 hover:border-orange-500',
    ...,
 },
  'green-500': {
 ringColor: 'ring-green-500/30 ring-offset-green-500',
 borderWithHover: 'border-green-500 hover:border-green-500',
    ...,
 },
  'blue-500': {
 ringColor: 'ring-blue-500/30 ring-offset-blue-500',
 borderWithHover: 'border-blue-500 hover:border-blue-500',
    ...,
  'gray-500': {
 ringColor: 'ring-gray-500/30 ring-offset-gray-500',
 borderWithHover: 'border-gray-500 hover:border-gray-500',
    ...,
 },
  ...,
};

export const MyComponent = () => {
  const selectedSignerStyles = useSelectedSignerStyles(selectedSigner, combinedStyles);

  return (
    <div
      className={cn(
 selectedSigner ? selectedSignerStyles.ringClass : selectedSignerStyles.borderClass,
 )}
    >
      <h1>Hello</h1>
    </div>
 );
};

Le code ci-dessus montre une solution naïve utilisant un objet combinedStyles contenant des classes TailwindCSS pour divers styles de composants (anneau, bordure, survol, etc.).

Les composants utiliseraient des hooks personnalisés pour appliquer les styles appropriés en fonction du destinataire sélectionné. Par exemple, le destinataire 1 utiliserait les styles green-500, turnant tous les éléments associés en vert.

Le problème avec cette approche est que nous ne pouvons pas importer l'objet combinedStyles dans d'autres composants car TailwindCSS supprimera les classes inutilisées. Cela signifie que nous devions copier et coller le même objet dans plusieurs fichiers. En conséquence, cela pollue le code avec du code dupliqué, ce qui rend plus difficile la maintenance et l'évolutivité du code. À mesure que l'application grandit, l'objet combinedStyles deviendra plus grand et plus complexe. De plus, ce n'est pas très flexible, car cela ne permet pas de personnaliser facilement les couleurs.

Bien que cette approche fonctionne, il existe une solution plus efficace et évolutive.

Solution : Modulariser la Logique et Utiliser des Variables CSS

Pour résoudre le défi de la réutilisation des couleurs à travers les composants, nous avons déplacé les couleurs et les hooks associés dans un fichier séparé, définissant les styles uniquement dans ce fichier et y accédant à partir des composants via des hooks personnalisés.

export const SIGNER_COLOR_STYLES = {
 green: {
 default: {
 background: 'bg-[hsl(var(--signer-green))]',
 base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]',
 fieldItem:
        'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10',
 fieldItemInitials:
        'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]',
 comboxBoxItem: 'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
 },
 },

  ...
};

export type CombinedStylesKey = keyof typeof SIGNER_COLOR_STYLES;

export const AVAILABLE_SIGNER_COLORS = [
  'green',
  'blue',
  'purple',
  'orange',
  'yellow',
  'pink',
] as const satisfies CombinedStylesKey[];

export const useSignerColors = (index: number) => {
  const key = AVAILABLE_SIGNER_COLORS[index % AVAILABLE_SIGNER_COLORS.length];

  return SIGNER_COLOR_STYLES[key];
};

export const getSignerColorStyles = (index: number) => {
  return useSignerColors(index);
};

Le fichier a été tronqué pour des raisons de lisibilité. Vous pouvez voir le code complet dans le fichier signer-colors.ts du dépôt Documenso.

L'objet SIGNER_COLOR_STYLES contient les styles pour chaque couleur, tels que les couleurs de fond, de bordure et de survol. En fonction de l'index du signataire, le hook useSignerColors récupère les styles pour une couleur spécifique. La fonction getSignerColorStyles est une fonction d'assistance qui renvoie les styles pour un signataire particulier.

Maintenant, les composants peuvent accéder aux couleurs et aux styles en utilisant des hooks personnalisés. Par exemple, pour obtenir les styles d'un signataire spécifique, le composant peut appeler le hook useSignerColors avec l'index du signataire.

const signerStyles = useSignerColors(recipientIndex);

Le hook renverra les styles pour ce signataire, qui peuvent ensuite être appliqués au composant. Par exemple, vous pouvez accéder à la couleur de fond du signataire en utilisant signerStyles.default.background.

Cette approche facilite la gestion des couleurs et des styles, car ils sont définis dans un seul fichier. Changer ou ajouter des couleurs peut être fait en un seul endroit, rendant le code plus modulaire et réutilisable.

Nous avons également opté pour des variables CSS pour définir les couleurs, permettant plus de flexibilité et de cohérence dans le style. Une seule variable CSS pour chaque couleur peut couvrir une large gamme d'états sans dépendre de multiples classes TailwindCSS. Par exemple, vous pouvez facilement définir l'opacité et la luminosité d'une couleur sans utiliser de multiples classes. Les variables CSS aident à aligner les couleurs avec nos directives de marque tout en simplifiant l'ensemble du processus de stylisation.

La Fin

Nous sommes heureux de voir les nouveaux champs avancés publiés car ils offrent à nos utilisateurs plus de flexibilité, de variété et d'options de personnalisation. La mise en œuvre des nouveaux champs a eu ses défis, mais nous les avons surmontés et avons appris d'eux. Nous sommes impatients de continuer à améliorer Documenso et à fournir à nos utilisateurs la meilleure expérience de signature de documents.