Trucs et astuces pour créer des composants d'interface utilisateur réutilisables

Photo de Farzard Nazifi

Dans cet article, je veux partager quelques trucs et astuces que j'utilise lors de la construction de notre bibliothèque frontale principale à l'aide d'Ember.js. N'ayant jamais été en contact avec lui auparavant, cela a été une excellente opportunité d'apprentissage. J'espère que vous l'apprécierez! Veuillez noter que le code utilisé pour illustrer les idées de l'article contient juste assez d'informations pour faire passer le message. Il utilise également une terminologie Ember.js, mais les concepts sont censés être indépendants du cadre.

Les objectifs

Pour le dire simplement, les exigences pour la construction de la bibliothèque sont les suivantes:

  1. Elle doit être productive.
  2. Il doit être maintenable.
  3. Elle doit être cohérente.

Les approches

Minimisez la logique métier

L'un des problèmes les plus fréquents que je rencontre sur les projets sont les composants qui contiennent beaucoup trop de logique. Ainsi, effectuer des tâches qui sont, théoriquement, hors de leur portée.

Avant d'implémenter une fonctionnalité, il est bon de décrire certaines des tâches dont le composant est responsable.

Imaginez que nous construisons un composant bouton.

J'aimerais pouvoir:

  • Indiquez de quel type de bouton il s'agit - Principal ou régulier
  • Informer le contenu affiché à l'intérieur du bouton (icône et texte)
  • Désactiver ou activer le bouton
  • Effectuer une action au clic

Ayant ce petit aperçu, séparez les différentes parties impliquées dans le processus de construction de ce composant. Essayez d'identifier où les choses pourraient être placées.

1 - Le type et le contenu sont spécifiques au composant, ils peuvent donc être placés dans le fichier du composant.

Étant donné que le type est - dans une certaine mesure - requis, ajoutons une vérification au cas où aucune valeur n'a été fournie.

const type = get (this, 'type');
types const = {
  primaire: 'btn - primaire',
  regular: 'btn - regular',
}
retour (type)? types [type]: types.regular;

J'aime mapper les propriétés dans un objet, car cela permet de redimensionner les choses sans trop d'effort - au cas où nous aurions besoin d'un bouton de danger ou de quelque chose du genre.

2 - L'état désactivé peut être trouvé sur différents composants comme une entrée. Afin d'éviter la répétition, ce comportement peut être déplacé dans un module ou n'importe quelle structure partagée - les gens l'appellent un mixin.

3 - L'action de clic peut être trouvée dans différents composants. Ainsi, il peut également être déplacé vers un autre fichier et ne doit contenir aucune logique à l'intérieur - il suffit d'appeler le rappel fourni par le développeur.

De cette façon, nous pouvons avoir une idée des cas que notre composant doit traiter tout en aidant à définir une architecture de base qui prend en charge l'expansion.

État d'interface utilisateur réutilisable distinct

Certaines interactions d'interface utilisateur sont communes à différents composants, comme:

  • Activer / désactiver - par exemple. boutons, entrées
  • Développer / rétrécir - par exemple. réduire, listes déroulantes
  • Afficher / masquer - à peu près tout

Ces propriétés sont souvent utilisées uniquement pour contrôler l'état visuel - espérons-le.

Maintenez une nomenclature cohérente dans les différents composants. Toutes les actions liées à un état visuel peuvent être déplacées vers un mixin.

/ * UIStateMixin * /
disable () {
  set (this, ‘disabled’, true);
  retourner ceci;
},
activer() {
  set (this, 'disabled', false ');
  retourner ceci;
},

Chaque méthode est uniquement responsable du basculement d'une variable particulière et retourne le contexte actuel pour le chaînage, comme:

bouton
  .disable ()
  .showLoadingIndicator ();

Cette approche peut être étendue. Il peut accepter différents contextes et contrôler des variables externes au lieu d'utiliser des variables internes. Par exemple:

_getCurrentDisabledAttr () {
  return (isPresent (get (this, 'disabled')))
    ? 'désactivé' / * Paramètre externe * /
    : 'isDisabled'; / * Variable interne * /
},
activer (contexte) {
  set (context || this, this._getCurrentDisabledAttr (), false);
  retourner ceci;
}

Résumé des fonctionnalités de base

Chaque composant contient certaines routines. Ces routines doivent être exécutées quel que soit l'objectif du composant. Par exemple, vérifier un rappel avant de le déclencher.

Ces méthodes par défaut peuvent également être déplacées vers leurs propres mixins, comme ceci:

/ * BaseComponentMixin * /
_isCallbackValid (callbackName) {
  const callback = get (this, callbackName);
  
  return !! (isPresent (callback) && typeof callback === 'function');
},
_handleCallback (rappel, params) {
  if (! this._isCallbackValid (callback)) {
    lancer une nouvelle erreur (/ * message * /);
  }
  this.sendAction (rappel, params);
},

Et puis inclus dans les composants.

/ * Composant * /
onClick (params) {
  this._handleCallback ('onClick', params);
}

Cela permet de garder votre architecture de base cohérente. Il permet également l'expansion et même l'intégration avec des logiciels tiers. Mais s'il vous plaît, ne soyez pas un abstracteur philosophe.

Composer des composants

Évitez autant que possible la fonctionnalité de réécriture. La spécialisation peut être réalisée. Cela peut se faire par composition et groupement. En plus de peaufiner des composants plus petits afin de créer de nouveaux composants.

Par exemple:

Composants de base: bouton, liste déroulante, entrée.
Bouton déroulant => bouton + déroulant
Saisie semi-automatique => entrée + liste déroulante
Sélectionnez => entrée (lecture seule) + liste déroulante

De cette façon, chaque composant a ses propres fonctions. Chacun gère son propre état et ses propres paramètres tandis que le composant wrapper gère sa logique spécifique.

Séparation des préoccupations à son meilleur.

Diviser les préoccupations

Lors de la composition de composants plus complexes, il est possible de diviser les problèmes. Vous pouvez diviser les problèmes entre différentes parties d'un composant

Supposons que nous créons un composant sélectionné.

{{form-select binding = productId items = items}}
articles = [
  {description: "Produit # 1", valeur: 1},
  {description: "Produit # 2", valeur: 2}
]

En interne, nous avons un composant d'entrée simple et une liste déroulante.

{{form-input binding = _description}}
{{ui-dropdown items = items onSelect = (action 'selectItem')}}

Notre tâche principale est de présenter la description à l'utilisateur, mais cela n'a aucun sens pour notre application - la valeur le fait.

Lorsque vous sélectionnez une option, vous divisez l'objet, en envoyant la description à notre entrée via une variable interne tout en poussant la valeur vers le contrôleur, en mettant à jour la variable liée.

Ce concept peut être appliqué aux composants où la valeur liée doit être transformée, comme un nombre, une saisie semi-automatique ou un champ de sélection. Les sélecteurs de date peuvent également implémenter ce comportement. Ils peuvent démasquer la date avant de mettre à jour la variable liée tout en présentant la valeur masquée à l'utilisateur.

Les risques augmentent à mesure que les transformations augmentent en complexité. Par une logique excessive ou par la nécessité de soutenir des événements - réfléchissez-y donc avant de mettre en œuvre cette approche.

Préréglages vs nouveaux composants

Il est parfois nécessaire d'optimiser les composants et les services afin de faciliter le développement. Ceux-ci sont livrés sous forme de préréglages ou de nouveaux composants.

Les préréglages sont des paramètres. Lorsqu'ils sont informés, ils définissent des valeurs prédéfinies sur le composant, simplifiant sa déclaration. Cependant, les nouveaux composants sont généralement des versions plus spécialisées des composants de base.

Le plus difficile est de savoir quand implémenter des presets ou créer de nouveaux composants. J'utilise les directives suivantes pour prendre cette décision:

Quand créer des préréglages

1 - Modèles d'utilisation répétitifs

Il y a des moments où un composant particulier est réutilisé à divers endroits avec les mêmes paramètres. Dans ces cas, j'aime privilégier les préréglages aux nouveaux composants, surtout lorsque le composant de base a un nombre excessif de paramètres.

/ * Mise en œuvre régulière * /
{{form-autocomplete
    binding = productId
    url = "produits" / * URL à récupérer * /
    labelAttr = "description" / * Attribut utilisé comme étiquette * /
    valueAttr = "id" / * Attribut utilisé comme valeur * /
    apiAttr = "produit" / * Param envoyé sur demande * /
}}
/ * Préréglages * /
{{form-autocomplete
    preset = "produit"
    binding = productId
}}

Les valeurs du préréglage ne sont définies que si le paramètre n'a pas été informé, en conservant sa flexibilité.

/ * Implémentation naïve du module de presets * /
const presets = {
  produit: {
    url: «produits»,
    labelAttr: ‘description’,
    valueAttr: ‘id’,
    apiAttr: «produit»,
  },
}
const attrs = presets [get (this, ‘preset’)];
Object.keys (attrs) .forEach ((prop) => {
  if (! get (this, prop)) {
    set (this, prop, attrs [prop]);
  }
});

Cette approche réduit les connaissances requises pour personnaliser votre composant. Simultanément, il facilite la maintenance en vous permettant de mettre à jour les valeurs par défaut en un seul endroit.

2 - Le composant de base est trop complexe

Lorsque le composant de base que vous utiliseriez pour créer un composant plus spécifique accepte trop de paramètres. Ainsi, sa création engendrerait certains problèmes. Par exemple:

  • Vous devez injecter la plupart, sinon la totalité, des paramètres du nouveau composant au composant de base. Comme de plus en plus de composants en dérivent, toute mise à jour du composant de base refléterait une énorme quantité de changements. Ainsi, conduisant à une incidence plus élevée de bogues.
  • Plus il y a de composants créés, plus il devient difficile de documenter et de mémoriser les différentes nuances. Cela est particulièrement vrai pour les nouveaux développeurs.

Quand créer de nouveaux composants

1 - Extension des fonctionnalités

Il est viable de créer un nouveau composant lors de l'extension des fonctionnalités à partir d'un composant plus simple. Il vous aide à empêcher la fuite de logique spécifique à un composant vers un autre composant. Ceci est particulièrement utile lors de l'implémentation d'un comportement supplémentaire.

/ * Déclaration * /
{{ui-button-dropdown items = items}}
/* Sous la capuche */
{{# ui-button onClick = (action 'toggleDropdown')}}
  {{label}}  
{{/ ui-button}}
{{#if isExpanded}}
  {{ui-dropdown items = items}}
{{/si}}

L'exemple ci-dessus utilise le composant bouton. Cela étend sa disposition pour prendre en charge une icône fixe tout en incluant un composant déroulant et son état de visibilité.

2 - Paramètres de décoration

Il existe une autre raison possible de créer de nouveaux composants. C'est à ce moment qu'il est nécessaire de contrôler la disponibilité des paramètres ou de décorer les valeurs par défaut.

/ * Déclaration * /
{{form-datepicker onFocus = (action 'doSomething')}}
/* Sous la capuche */
{{form-input onFocus = (action '_onFocus')}}
_onFocus () {
  $ (this.element)
    .find ('entrée')
    .sélectionner(); / * Sélectionnez la valeur du champ sur le focus * /
  this._handleCallback ('onFocus'); / * Déclenche le rappel des paramètres * /
}

Dans cet exemple, il a été fourni au composant une fonction destinée à être appelée lorsque le champ est focalisé.

En interne, au lieu de passer directement le rappel au composant de base, il transmet une fonction interne. Cela effectue une tâche particulière (sélection de la valeur du champ), puis appelle le rappel fourni.

Il ne redirige pas tous les paramètres acceptés par le composant d'entrée de base. Cela permet de contrôler l'étendue de certaines fonctionnalités. Cela évite également les validations inutiles.

Dans mon cas, l'événement onBlur a été remplacé par un autre événement - onChange. Cela se déclenche lorsque l'utilisateur remplit le champ ou sélectionne une date sur le calendrier.

Conclusion

Lors de la construction de vos composants, tenez compte de votre côté ainsi que de celui qui utilise ce composant dans sa vie quotidienne. De cette façon, tout le monde gagne.

Le meilleur résultat vient de tout le monde dans le groupe qui fait ce qui est le mieux pour lui et le groupe - John Nash

N'ayez pas honte de demander des commentaires. Vous trouverez toujours quelque chose sur lequel travailler.

Pour affiner encore vos compétences en génie logiciel, je vous recommande de suivre la série «Composing Software» d'Eric Elliott. C’est génial!

Eh bien, j'espère que l'article vous a plu. Prenez ces concepts, transformez-les en vos propres idées et partagez-les avec nous!

N'hésitez pas non plus à me contacter sur twitter @gcolombo_! J'adorerais entendre votre opinion et même travailler ensemble.

Merci!