6 points par GN⁺ 2024-04-27 | 3 commentaires | Partager sur WhatsApp
  • Même lorsqu’ils sont associés au même élément, les attributs HTML et les propriétés DOM fonctionnent comme des espaces de stockage distincts, si bien qu’une modification de l’un n’est pas forcément répercutée automatiquement sur l’autre
  • Les attributs apparaissent dans la sérialisation HTML et dans le panneau des éléments des outils de développement, tandis que les propriétés relèvent d’un état propre au DOM et n’apparaissent pas dans des résultats comme outerHTML
  • Les valeurs d’attribut sont toujours des chaînes de caractères et leurs noms ne sont pas sensibles à la casse, alors que les propriétés peuvent avoir différents types, y compris des objets, et que leurs noms sont sensibles à la casse
  • Il existe des propriétés qui reflètent des attributs comme id, className, htmlFor, ariaLabel, mais aussi des exceptions où l’état courant et la configuration par défaut sont séparés, comme input.value
  • Selon les frameworks, la cible de l’affectation diffère : Preact et VueJS privilégient généralement les propriétés, React privilégie généralement les attributs, et Lit distingue explicitement les deux avec la syntaxe .value

Les attributs et les propriétés peuvent différer même s’ils portent le même nom

  • Les attributs et les propriétés peuvent se comporter comme des espaces de stockage distincts
    • Dans ``, getAttribute('foo') renvoie 'bar', alors que div.foo peut valoir undefined
    • Même après div.foo = 'hello world', la valeur de l’attribut foo reste 'bar'
  • Même si un template de framework utilise une syntaxe qui ressemble à un attribut, comme ``, il peut en interne définir une propriété selon le cas
    • Le choix de définir une propriété dépend du framework
    • Certaines affectations peuvent avoir des effets de bord qui modifient à la fois la propriété et l’attribut

Ce qui reste dans la sérialisation HTML, ce sont les attributs

  • Les attributs existent à la fois dans le HTML et le DOM, alors que les propriétés n’existent que dans le DOM
  • Même après avoir exécuté div.setAttribute('foo', 'bar') et div.hello = 'world', seul l’attribut apparaît dans div.outerHTML, comme `

`

  • Le panneau des éléments des outils de développement du navigateur affiche les attributs de l’élément, pas l’ensemble de ses propriétés

Type des valeurs et gestion de la casse

  • Les valeurs d’attribut doivent fonctionner dans un format sérialisé, elles sont donc toujours des chaînes de caractères
    • Si on passe un objet à setAttribute, il est stocké sous forme de chaîne "[object Object]"
    • Le même objet affecté à une propriété peut conserver son type objet
  • Les noms d’attribut ne sont pas sensibles à la casse
    • HeLlO="world" peut être normalisé en attribut hello dans le DOM
    • setAttribute('FOO', 'bar') apparaît également comme l’attribut foo
  • Les noms de propriété, eux, sont sensibles à la casse
    • Même après div.TeSt = 'value', div.test peut valoir undefined
  • Contrairement aux noms d’attribut, les valeurs d’attribut sont sensibles à la casse

Les propriétés qui reflètent les attributs

  • Certaines propriétés reflètent un attribut donné
    • div.id agit comme un getter/setter qui lit et écrit l’attribut id
    • Si on exécute div.id = 'bar', l’attribut id passe lui aussi à 'bar'
  • Dans une relation de réflexion, c’est l’attribut qui constitue la source de vérité
    • Définir la propriété met à jour l’attribut
    • Lire la propriété revient à lire la valeur de l’attribut
  • Les attributs définis dans les spécifications disposent souvent d’une propriété correspondante par commodité
    • Un attribut arbitraire foo n’étant pas défini par une spécification, une propriété foo correspondante n’est pas créée automatiquement
  • La spécification de `` définit les attributs dans “Content attributes” et les propriétés dans “DOM interface”
    • Les propriétés IDL reversed et type doivent refléter les attributs de contenu du même nom

Quand le nom change dans la relation de réflexion

  • Certaines propriétés ont un nom différent de celui de l’attribut qu’elles reflètent
  • Ce changement peut servir à mieux correspondre au style des propriétés JavaScript
    • Sur ``, el.crossOrigin reflète l’attribut crossorigin
    • Sur tous les éléments, el.ariaLabel reflète l’attribut aria-label
    • Les propriétés de réflexion ARIA sont devenues compatibles tous navigateurs fin 2023 ; avant cela, seuls les attributs pouvaient être utilisés
  • Dans d’autres cas, le nom diffère à cause d’anciens mots réservés de JavaScript
    • Sur tous les éléments, el.className reflète l’attribut class
    • Sur ``, el.htmlFor reflète l’attribut for

Validation, conversion de type et valeurs par défaut

  • Les propriétés peuvent intégrer une validation et des valeurs par défaut, contrairement aux attributs eux-mêmes
  • input.type illustre bien la différence entre attribut et propriété
    • Même si input.getAttribute('type') vaut null, input.type renvoie la valeur par défaut 'text'
    • input.type = 'number' donne 'number' côté attribut comme côté propriété
    • input.type = 'foo' écrit 'foo' dans l’attribut, mais le getter de la propriété considère cette valeur comme invalide et renvoie 'text'
  • Certaines propriétés effectuent une conversion de type
    • Sur ``, details.getAttribute('open') est une chaîne vide, alors que details.open vaut true
    • details.open = false supprime l’attribut open
    • details.open = 'hello' n’écrit pas la chaîne telle quelle dans l’attribut : la valeur est convertie en booléen, ce qui laisse l’attribut open présent
  • Des propriétés comme img.height convertissent la valeur de l’attribut en nombre
    • Le setter convertit la valeur reçue en nombre et traite les valeurs négatives comme 0

input.value sépare la valeur courante et la valeur par défaut

  • input possède à la fois une propriété value et un attribut value, mais la propriété value ne reflète pas l’attribut value
  • La propriété qui reflète l’attribut value est defaultValue
    • Le getter de defaultValue renvoie l’attribut value ou une chaîne vide
    • Le setter de defaultValue définit l’attribut value sous forme de chaîne
  • La propriété value suit d’abord defaultValue, puis utilise une valeur interne dès qu’elle est définie par JavaScript ou par une interaction utilisateur
    • Au départ, dans ``, value, defaultValue et getAttribute('value') valent tous 'default'
    • Après input.defaultValue = 'new default', l’attribut et value semblent tous deux valoir 'new default'
    • Ensuite, après input.value = 'hello!', value devient 'hello!', tandis que l’attribut et defaultValue restent à 'new default'
    • Même après setAttribute('value', 'another new default'), la valeur courante de value reste 'hello!'
  • Lors d’une réinitialisation du formulaire, l’état interne de value peut être réinitialisé

Les attributs conviennent mieux à la configuration, les propriétés à l’état

  • Il est naturel de réserver les attributs à la configuration et les propriétés à l’état
  • `` en est un bon exemple, sauf pour le nommage
    • L’attribut value définit la valeur par défaut
    • La propriété value fournit l’état courant
  • La distinction selon laquelle la validation s’applique lors de la lecture ou de l’écriture d’une propriété, mais pas lors de la lecture ou de l’écriture d’un attribut, va dans le même sens
  • et expriment leur état ouvert avec l’attribut open, que le navigateur peut lui-même ajouter ou supprimer selon les interactions utilisateur
    • Cette conception brouille la distinction selon laquelle les attributs servent à la configuration
    • Les frameworks qui maintiennent le DOM, comme le JavaScript classique, doivent donc gérer le fait que le DOM puisse se modifier lui-même
  • Une autre conception pourrait séparer un attribut de configuration par défaut comme ``, une propriété details.open pour lire et écrire l’état courant, et une pseudo-classe CSS pour cibler cet état
  • Simon Peters a retrouvé une discussion de conception initiale à ce sujet
  • contenteditable brouille lui aussi cette distinction, mais il est traité comme un mécanisme opt-in qui accepte beaucoup de comportements cassants

Approche selon les frameworks

  • Preact et VueJS définissent une propriété lorsque propName in element est vrai, sauf pour quelques exceptions prédéfinies ; sinon, ils définissent un attribut
    • Ils privilégient donc les propriétés plutôt que les attributs
    • Les méthodes de rendu en chaîne de caractères font l’inverse et ignorent les éléments réservés aux propriétés
    • Implémentations associées : setProperty de Preact, shouldSetAsProp de VueJS
  • React définit généralement des attributs, sauf pour certaines exceptions prédéfinies
    • Cette approche produit une logique proche de celle du rendu en chaîne de caractères
    • Les propriétés des éléments personnalisés ne figurant pas dans la liste prédéfinie de React, elles sont définies comme attributs, et les API réservées aux propriétés peuvent ne pas fonctionner
    • Avec React 19, les éléments personnalisés doivent adopter le modèle de Preact/VueJS
    • React a popularisé le nom de propriété className dans une syntaxe qui ressemble à un attribut, mais en interne il définit l’attribut class
    • Implémentation associée : setProp de React
  • lit-html conserve la différence entre attributs et propriétés dans sa syntaxe
    • En préfixant le nom par ., comme dans ``, on définit une propriété et non un attribut
    • Documentation associée : Lit expressions

3 commentaires

 
GN⁺ 2024-04-27
Avis sur Hacker News
  • Utiliser les attributs data-* constitue un bon compromis entre les approches abordées dans l’article
    Ces attributs sont automatiquement exposés en JavaScript comme propriétés en lecture/écriture de dataset, et sont donc accessibles sous une forme comme document.getElementById('myDiv').dataset.payload == "something"
    Il faut toutefois faire attention au fait que les attributs HTML sont en kebab-case, tandis que JavaScript utilise le camelCase, si bien que data-my-cool-data-attribute devient dataset.myCoolDataAttribute

    • Je déteste vraiment la conversion automatique de casse
      Dans les différents écosystèmes qui utilisent le camelCase, il n’existe pas de convention bien établie sur la manière de traiter les acronymes en majuscules, ce qui rend des cas comme dataset.myID ambigus
    • Pour éviter la conversion entre kebab-case et camelCase, j’utilise des méthodes comme Element.hasAttribute('data-thing'), Element.getAttribute('data-thing'), Element.setAttribute('data-thing', '...')
      Quand on inspecte ou débogue le DOM, cela évite de passer par une conversion maladroite et rend l’intention beaucoup plus claire
    • La documentation MDN sur les data attributes couvre la syntaxe HTML, l’accès depuis JavaScript, l’accès depuis CSS et les points d’attention liés à l’accessibilité : https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
      Par ailleurs, https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att... décrit les attributs globaux data-* comme des « attributs de données personnalisés qui permettent aux scripts d’échanger des informations propres entre HTML et sa représentation DOM »
    • Ces attributs sont aussi accessibles dans une certaine mesure depuis CSS
    • C’est également une autre manière intéressante de communiquer entre contextes d’exécution JavaScript
      Par contexte, il ne s’agit pas forcément de différents iframe, mais aussi de cas comme les extensions de navigateur, qui créent un contexte d’exécution JavaScript séparé accédant au même DOM
      Dans la terminologie du protocole des outils de développement, on appelle aussi cela des isolated worlds
  • Ce dont parle l’article illustre une complexité et des comportements qui n’apportent pas grand-chose dans la vie quotidienne
    Ces comportements n’ont que des inconvénients et aucun avantage ; la spécification aurait dû simplement définir les objets DOM et les objets JavaScript comme étant la même chose, et faire des attributs et des propriétés un seul et même concept

    • Si une telle restriction avait « simplement » existé, la plupart des frameworks UI modernes ne fonctionneraient pas du tout
      Le fait de pouvoir stocker des valeurs qui ne sont pas des chaînes sur des nœuds DOM est un avantage évident, et ce n’est pas un point à balayer d’un revers de main
    • Je suis d’accord, à part sur le mot « simplement »
      Ce n’est pas simple du tout, sauf à exiger que toutes les propriétés ne puissent être que des chaînes
      Il faut aussi prendre en compte les conversions avec les nombres, les propriétés pouvant recevoir des objets, et les propriétés qui ne correspondent à aucun attribut
      Cela dit, il aurait été préférable d’avoir une convention ou une règle figée à 100 %, mais ce n’est pas une chose simple
    • Avec cela, on ne pourrait pas mettre de valeurs non textuelles dans l’interface DOM, et comme elle inclut aussi des méthodes, la plupart des fonctionnalités du DOM disparaîtraient
    • Rendre les attributs et les propriétés symétriques est difficile avec l’API DOM existante
      Des méthodes comme addEventListener sont des propriétés du prototype des éléments DOM individuels, mais vues comme des attributs textuels, cela n’a aucun sens
      Les méthodes des éléments DOM devraient alors devenir des fonctions libres comme HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));, et même si c’est une question de préférence, il est appréciable de pouvoir utiliser en JavaScript des appels de méthodes sur des objets
    • Je ne vois pas très bien pourquoi les deux devraient être différents
      Les éléments DOM disposent d’une interface très spécifique, setAttribute, pour modifier leur état, et je ne vois pas pourquoi on devrait s’attendre à ce qu’une propriété ajoutée directement à l’objet JavaScript conçu pour envoyer des messages au DOM soit automatiquement routée vers cette interface
      Une telle API pourrait offrir une meilleure expérience, mais elle soulève quelques difficultés, même si elle serait sans doute plus propre
      Peut-être est-ce parce que j’ai écrit du code backend pendant trop longtemps, mais face à ce type d’interface, j’ai l’impression qu’il se passe beaucoup de choses étranges en interne, et qu’un gentil développeur du passé a fourni le wrapper le plus sûr possible
  • Un point de vue utile pour comprendre est qu’au sens strict, un attribut est un nœud DOM, c’est-à-dire un nœud Attr
    Ce nœud peut avoir des propriétés comme name, value, ownerElement, prefix, et l’ensemble est accessible non pas sous forme de tableau, mais sous forme de NamedNodeMap, via la propriété attributes de l’élément propriétaire
    Le nom getAttribute(name) est donc trompeur : en réalité, il ne renvoie pas le nœud d’attribut lui-même, mais la propriété value de ce nœud, ce qui le rapproche davantage de getAttributeValue(name)
    Références : https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Attr

    • Cette explication est correcte, mais je ne sais pas s’il existe quelque chose de pratique qu’on ne puisse pas faire avec setAttribute, getAttribute, getAttributeNames, hasAttribute, et qui nécessite les nœuds d’attribut ou el.attributes
      On ne peut pas non plus déplacer des nœuds d’attribut entre éléments, ni changer l’ordre des nœuds d’attribut au sein d’un élément
  • Jake, alors on fait quoi de ça : https://jakearchibald.com/2023/against-self-closing-tags-in-...

    • « Vous verrez cette syntaxe sur mon blog, parce que Prettier la produit ainsi, et j’aime vraiment beaucoup Prettier »
  • Je comprends l’argument consistant à ne pas mettre à jour les propriétés, mais le fait de pouvoir cibler ces éléments avec du CSS est utile

    • C’est pour cela que je disais qu’il devrait aussi y avoir des pseudo-classes, un peu comme :checked pour les éléments de saisie
    • Pour moi aussi, ce cas pèse plus que toutes les autres raisons
      Il est beaucoup plus facile de déboguer en inspectant immédiatement le DOM qu’en passant par le débogueur
  • La différence la plus importante est probablement la persistance, mais l’article n’en parle pas
    Les attributs sont conservés tant qu’on peut accéder au même objet racine de cet arbre DOM, par exemple tant qu’on ne quitte pas la page
    Les propriétés d’objet ne sont conservées que jusqu’à ce que cet objet soit collecté par le garbage collector

    • L’objet existe tant que le nœud DOM qui possède l’attribut existe, donc le cycle de vie est le même
      Si le nœud DOM est cloné, ou s’il passe par une sérialisation/désérialisation, il devient un nœud DOM complètement différent avec les mêmes attributs, et n’a donc pas les propriétés non par défaut de l’autre nœud
    • Ici, il n’y a pas de différence de comportement concrète en dehors de la différence existante entre attributs et propriétés
    • Je suis curieux de voir comment cette différence se manifeste réellement
  • Article intéressant
    J’ai toujours vu les éléments DOM en JavaScript comme des proxys
    Ils permettent de travailler avec les vrais éléments DOM, mais comme ce sont aussi simplement des objets JavaScript, j’avais l’impression qu’en plus d’utiliser les fonctions qui interagissent avec les éléments réels, on pouvait leur assigner des valeurs et en faire ce qu’on voulait

  • C’est agréable à lire, bien expliqué avec des exemples, et cela devrait aider beaucoup de monde, surtout avec plusieurs détails qu’on oublie souvent

  • Les propriétés DOM sont aussi appelées IDL attributes

    • C’est vrai, mais j’ai délibérément évité ce terme
      En dehors des auteurs de spécifications, peu de gens disent IDL attributes, et si on appelle les deux « attributs », cela devient très confus
  • Bon article, et je me demande à quoi pourrait ressembler une meilleure façon d’exprimer tout cela
    Il serait intéressant que le HTML puisse avoir des attributs qui ne soient pas des chaînes de caractères, et dont la valeur référence des valeurs locales d’une balise script avec portée, par exemple
    La façon de voir les attributs comme des réglages par défaut m’a semblé un peu étrange
    Par exemple, si l’attribut class ne montrait que la valeur par défaut, alors lorsqu’on bascule des classes, ce serait assez bizarre de ne pas voir les classes actives, ou de voir des classes inactives, dans l’inspecteur DOM ou dans le HTML converti en chaîne

    • Je ne disais pas « réglage par défaut », mais simplement réglage
      L’attribut class fonctionne bien dans ce modèle
      En tant que propriétaire du document, je définis l’attribut class, et je peux modifier ce réglage au fil du temps, mais je suis le seul à le faire
      Dans un autre modèle, le navigateur ajouterait et retirerait lui-même des classes en réaction aux états hover ou focus ; heureusement, il ne le fait pas, et on utilise des pseudo-classes
 
superwoou 2024-04-27

Ils ont traduit à la fois attribute et property par le même mot, « attribut »...