6 puntos por GN⁺ 2024-04-27 | 3 comentarios | Compartir por WhatsApp
  • Aunque estén en el mismo elemento, los atributos de HTML y las propiedades del DOM pueden funcionar como espacios de almacenamiento separados, por lo que un cambio en uno no siempre se refleja automáticamente en el otro.
  • Los atributos aparecen en la serialización HTML y en el panel de elementos de las herramientas de desarrollo, pero las propiedades son estado exclusivo del DOM, así que no aparecen en resultados como outerHTML.
  • Los valores de los atributos siempre son cadenas y sus nombres no distinguen entre mayúsculas y minúsculas, mientras que las propiedades pueden tener distintos tipos, incluidos objetos, y sus nombres sí distinguen mayúsculas de minúsculas.
  • Hay propiedades que reflejan atributos, como id, className, htmlFor y ariaLabel, pero también hay excepciones como input.value, donde el estado actual y la configuración predeterminada están separados.
  • Cada framework apunta a cosas distintas: Preact y VueJS suelen priorizar propiedades, React suele priorizar atributos, y Lit los distingue explícitamente con la sintaxis .value.

Atributos y propiedades pueden ser distintos aunque tengan el mismo nombre

  • Los atributos y las propiedades pueden funcionar como si fueran espacios de almacenamiento distintos.
    • En ``, getAttribute('foo') devuelve 'bar', pero div.foo puede ser undefined.
    • Después, aunque se haga div.foo = 'hello world', el valor del atributo foo sigue siendo 'bar'.
  • En las plantillas de los frameworks, aunque se use una sintaxis que parece atributo, como ``, internamente en algunos casos se puede estar configurando una propiedad.
    • Qué casos terminan configurando una propiedad depende de cada framework.
    • Algunas configuraciones pueden tener efectos secundarios y cambiar tanto la propiedad como el atributo.

Lo que queda en la serialización HTML son los atributos

  • Los atributos existen tanto en HTML como en el DOM, pero las propiedades existen solo en el DOM.
  • Aunque se ejecuten div.setAttribute('foo', 'bar') y div.hello = 'world', en div.outerHTML solo aparecen los atributos, como en `

`.

  • El panel de elementos de las herramientas de desarrollo del navegador muestra los atributos del elemento, no todas sus propiedades.

Tipo de valor y manejo de mayúsculas/minúsculas

  • Los valores de los atributos siempre son cadenas porque deben funcionar en un formato serializado.
    • Si se pasa un objeto a setAttribute, se guarda como la cadena "[object Object]".
    • Si ese mismo objeto se asigna a una propiedad, puede conservarse como objeto.
  • Los nombres de atributos no distinguen mayúsculas y minúsculas.
    • HeLlO="world" puede normalizarse como el atributo hello en el DOM.
    • setAttribute('FOO', 'bar') también aparece como el atributo foo.
  • Los nombres de propiedades sí distinguen mayúsculas y minúsculas.
    • Aunque se haga div.TeSt = 'value', div.test puede ser undefined.
  • A diferencia del nombre del atributo, el valor del atributo sí distingue mayúsculas y minúsculas.

Propiedades que reflejan atributos

  • Algunas propiedades reflejan un atributo específico.
    • div.id funciona como un getter/setter que lee y escribe el atributo id.
    • Si se ejecuta div.id = 'bar', el atributo id también cambia a 'bar'.
  • En una relación de reflejo, el atributo es la fuente de verdad.
    • Al asignar la propiedad, se actualiza el atributo.
    • Al leer la propiedad, se lee el valor del atributo.
  • Los atributos definidos en la especificación suelen tener una propiedad correspondiente por conveniencia.
    • Un atributo arbitrario como foo no está definido en la especificación, así que no se crea automáticamente una propiedad foo correspondiente.
  • La especificación de `` define los atributos en “Content attributes” y las propiedades en “DOM interface”.
    • Las propiedades IDL reversed y type deben reflejar los atributos de contenido con el mismo nombre.

Relaciones de reflejo donde cambia el nombre

  • Algunas propiedades tienen un nombre distinto al del atributo que reflejan.
  • A veces el nombre cambia para ajustarse al estilo de propiedades de JavaScript.
    • En ``, el.crossOrigin refleja el atributo crossorigin.
    • En cualquier elemento, el.ariaLabel refleja el atributo aria-label.
    • Las propiedades de reflejo de ARIA se volvieron multiplataforma entre navegadores a finales de 2023; antes de eso solo podían usarse atributos.
  • En otros casos, el nombre cambió por antiguas palabras reservadas de JavaScript.
    • En cualquier elemento, el.className refleja el atributo class.
    • En ``, el.htmlFor refleja el atributo for.

Validación, conversión de tipo y valores predeterminados

  • Las propiedades pueden tener validación y valores predeterminados, pero los atributos por sí solos no tienen ese comportamiento.
  • input.type muestra muy bien la diferencia entre atributo y propiedad.
    • Aunque input.getAttribute('type') sea null, input.type devuelve el valor predeterminado 'text'.
    • input.type = 'number' se ve como 'number' tanto en el atributo como en la propiedad.
    • input.type = 'foo' pone 'foo' en el atributo, pero el getter de la propiedad detecta que no es válido y devuelve 'text'.
  • Algunas propiedades hacen conversión de tipo.
    • En ``, details.getAttribute('open') es una cadena vacía y details.open es true.
    • details.open = false elimina el atributo open.
    • details.open = 'hello' no guarda la cadena tal cual en el atributo, sino que la convierte a boolean y deja presente el atributo open.
  • Propiedades como img.height convierten el valor del atributo a número.
    • El setter convierte a número el valor recibido y trata los negativos como 0.

input.value separa el valor actual del valor predeterminado

  • En input existen tanto la propiedad value como el atributo value, pero la propiedad value no refleja el atributo value.
  • La propiedad que sí refleja el atributo value es defaultValue.
    • El getter de defaultValue devuelve el atributo value o una cadena vacía.
    • El setter de defaultValue asigna al atributo value una cadena.
  • La propiedad value sigue inicialmente a defaultValue, pero una vez que se cambia desde JavaScript o por interacción del usuario, pasa a usar un valor interno.
    • En un `` inicial, value, defaultValue y getAttribute('value') valen 'default'.
    • Si se ejecuta input.defaultValue = 'new default', tanto el atributo como value se ven como 'new default'.
    • Después, si se ejecuta input.value = 'hello!', value pasa a ser 'hello!', pero el atributo y defaultValue siguen siendo 'new default'.
    • Aunque luego se haga setAttribute('value', 'another new default'), el value actual sigue siendo 'hello!'.
  • Al resetear un formulario, el estado interno de value puede volver a inicializarse.

Los atributos son más naturales para configuración, las propiedades para estado

  • Es natural separar atributos para configuración y propiedades para estado.
  • `` encaja con esta idea, salvo por el nombre.
    • El atributo value define el valor predeterminado.
    • La propiedad value expone el estado actual.
  • También encaja con esta separación el hecho de que al leer o escribir propiedades sí se aplique validación, mientras que con atributos no.
  • y expresan el estado abierto con el atributo open, y el navegador puede agregar o quitar ese atributo directamente según la interacción del usuario.
    • Ese diseño rompe la idea de que los atributos son solo para configuración.
    • Los frameworks que mantienen el DOM o el código JavaScript general deben contemplar que el DOM puede cambiar por sí mismo.
  • Un diseño alternativo podría separar un atributo de configuración predeterminada como ``, una propiedad details.open para leer y escribir el estado actual, y una pseudoclase de CSS para apuntar al estado.
  • Simon Peters encontró una discusión de diseño inicial relacionada con esto.
  • contenteditable también rompe esta separación, pero se trata como un opt-in que acepta bastante ruptura.

Cómo lo manejan los frameworks

  • Preact y VueJS, salvo algunas excepciones predefinidas, configuran una propiedad cuando propName in element es verdadero; si no, configuran un atributo.
    • Por defecto prefieren propiedades sobre atributos.
    • Sus métodos de renderizado a cadena hacen lo contrario e ignoran lo que sea exclusivo de propiedades.
    • Implementaciones relacionadas: setProperty de Preact, shouldSetAsProp de VueJS
  • React, salvo algunas excepciones predefinidas, configura atributos.
    • Ese enfoque crea una lógica parecida a la de sus métodos de renderizado a cadena.
    • Como las propiedades de elementos personalizados no están en la lista predefinida de React, se asignan como atributos y las APIs exclusivas de propiedades pueden no funcionar.
    • En React 19 está previsto cambiar al modelo de Preact/VueJS para elementos personalizados.
    • React popularizó el nombre de propiedad className con una sintaxis que parece atributo, pero internamente configura el atributo class.
    • Implementación relacionada: setProp de React
  • lit-html mantiene la diferencia entre atributos y propiedades en la sintaxis.
    • Si se antepone . al nombre, como en ``, se configura una propiedad y no un atributo.
    • Documentación relacionada: Lit expressions

3 comentarios

 
GN⁺ 2024-04-27
Opiniones en Hacker News
  • Usar atributos data-* es un buen punto medio entre los enfoques tratados en el artículo.
    Estos atributos se exponen automáticamente en JavaScript como propiedades de lectura/escritura de dataset, por lo que se puede acceder a ellos como document.getElementById('myDiv').dataset.payload == "something".
    Eso sí, hay que tener cuidado con que los atributos HTML usan kebab-case y JavaScript usa camelCase, así que data-my-cool-data-attribute se convierte en dataset.myCoolDataAttribute.

    • De verdad detesto la conversión automática entre estilos de mayúsculas y minúsculas.
      En varios ecosistemas que usan camelCase no hay una convención establecida sobre cómo tratar las siglas en mayúsculas, así que casos como dataset.myID quedan ambiguos.
    • Para evitar la conversión entre kebab-case y camelCase, uso métodos como Element.hasAttribute('data-thing'), Element.getAttribute('data-thing') y Element.setAttribute('data-thing', '...').
      Al inspeccionar o depurar en el DOM, no hace falta pasar por una conversión incómoda, así que la intención queda mucho más clara.
    • La documentación de MDN sobre data attributes cubre la sintaxis HTML, el acceso desde JavaScript, el acceso desde CSS y consideraciones de accesibilidad: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Us...
      Además, en https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att... describe los atributos globales data-* como “atributos de datos personalizados que permiten a los scripts intercambiar información propia entre HTML y su representación en el DOM”.
    • Estos atributos también son accesibles en cierta medida desde CSS.
    • También son otra forma interesante de comunicarse entre contextos de ejecución de JavaScript.
      Los contextos aquí no se refieren necesariamente a distintos iframe, sino a casos como las extensiones de navegador, que crean contextos de ejecución de JavaScript separados con acceso al mismo DOM.
      En la terminología del protocolo de herramientas para desarrolladores, a esto también se le llama isolated worlds.
  • Lo que se trata en el artículo representa complejidad y comportamientos que no aportan mucho a la vida.
    Ese comportamiento solo tiene desventajas y ninguna ventaja; la especificación simplemente debería haber definido los objetos DOM y los objetos JavaScript como la misma cosa, haciendo que atributos y propiedades fueran también el mismo concepto.

    • Si esa restricción “simplemente” existiera, la mayoría de los frameworks modernos de UI no funcionarían en absoluto.
      Poder almacenar valores que no sean strings en nodos DOM es una ventaja clara, y no es algo que deba despacharse tan a la ligera.
    • Estoy de acuerdo, salvo por la parte de “simplemente”.
      Mientras no se exija que todas las propiedades solo puedan ser strings, no es nada simple.
      Hay que considerar conversiones con números, propiedades que pueden establecer objetos y propiedades que no tienen un atributo correspondiente.
      Aun así, habría sido mejor tener una convención o regla 100% fija, pero eso no es algo simple.
    • Si se hiciera así, no se podrían poner valores que no sean strings en la interfaz del DOM y, como también incluye métodos, desaparecería la mayor parte de la funcionalidad del DOM.
    • Hacer que atributos y propiedades sean simétricos entre sí es difícil con la API DOM existente.
      Métodos como addEventListener son propiedades del prototipo de cada elemento DOM, pero vistos como atributos de string no tienen sentido.
      Entonces los métodos de elementos DOM tendrían que convertirse en funciones libres como HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));; aunque es cuestión de gustos, en JavaScript es mejor poder usar llamadas a métodos de objeto.
    • No entiendo bien por qué tienen que ser distintos.
      Los elementos DOM tienen una interfaz muy específica que cambia el estado con setAttribute, y no veo por qué habría que esperar que, al pegarle simplemente una propiedad al objeto JavaScript creado para enviar mensajes al DOM mediante esa interfaz, esta se enrute automáticamente.
      Una API así podría ofrecer una mejor experiencia, pero tiene varios problemas difíciles; aun así, probablemente sería más limpia.
      Quizá sea porque llevo demasiado tiempo escribiendo código backend, pero al ver este tipo de interfaz siento que por dentro pasan muchas cosas raras y que algún desarrollador amable del pasado nos dio el wrapper más seguro posible.
  • Una perspectiva útil para entenderlo es que, en sentido estricto, un atributo es un nodo DOM, es decir, un nodo Attr.
    Ese nodo puede tener propiedades como name, value, ownerElement y prefix, y en conjunto se puede acceder a él como el valor de la propiedad attributes del elemento propietario, en forma de NamedNodeMap y no de arreglo.
    Por eso el nombre getAttribute(name) puede ser engañoso: en realidad no devuelve el nodo de atributo en sí, sino la propiedad value de ese nodo de atributo, así que se parece más a getAttributeValue(name).
    Referencias: 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

    • Esa explicación es correcta, pero no sé si hay algo práctico que no se pueda hacer con setAttribute, getAttribute, getAttributeNames y hasAttribute, y que solo pueda hacerse con nodos de atributo o el.attributes.
      Tampoco se pueden mover nodos de atributo entre elementos ni cambiar el orden de los nodos de atributo dentro de un elemento.
  • Jake, entonces, ¿qué hacemos con esto?: https://jakearchibald.com/2023/against-self-closing-tags-in-...

    • “Verán esta sintaxis en mi blog, pero eso es porque Prettier lo hace así y a mí me gusta mucho Prettier”
  • Entiendo el argumento de no actualizar las propiedades, pero es útil que se pueda apuntar a ellas con CSS

    • Por eso decía que también debería haber una pseudoclase, similar a :checked en los elementos de entrada
    • Para mí también este caso pesa más que todos los demás motivos
      Es mucho más fácil depurar revisando el DOM de inmediato que pasar por el depurador
  • Probablemente la diferencia más importante sea la persistencia, pero el artículo no la trata
    Los atributos se mantienen mientras se pueda acceder al mismo objeto raíz de ese árbol DOM, por ejemplo mientras no se salga de la página
    Las propiedades de objeto solo se mantienen hasta que ese objeto sea recolectado por el recolector de basura

    • El objeto existe mientras exista el nodo DOM que tiene el atributo, así que el ciclo de vida es el mismo
      Si el nodo DOM se clona o pasa por serialización/deserialización, se convierte en un nodo DOM completamente distinto con los mismos atributos, por lo que no tendrá las propiedades que no sean valores predeterminados de otro nodo
    • Aquí no hay una diferencia práctica de comportamiento más allá de la diferencia ya existente entre atributos y propiedades
    • Me da curiosidad cómo se manifiesta realmente esa diferencia
  • Es un artículo interesante
    Siempre vi los elementos DOM en JavaScript como una especie de proxy
    Te permiten trabajar con el elemento DOM real, pero al mismo tiempo, como son simplemente objetos JavaScript, sentía que además de usar funciones que interactúan con el elemento real, también se les pueden asignar valores y hacer lo que uno quiera

  • Está bien explicado con ejemplos y se lee bien; en particular, incluye varios detalles que a menudo se pasan por alto, así que podría ser útil para mucha gente

  • Las propiedades DOM también se llaman IDL attributes

    • Es cierto, pero evité ese término a propósito
      Fuera de quienes escriben especificaciones, casi nadie las llama IDL attributes, y si a ambas cosas se les dice atributos, se vuelve muy confuso
  • Es un buen artículo, y me deja con curiosidad sobre cómo sería una mejor forma de expresarlo
    Sería interesante que HTML pudiera tener atributos que no fueran cadenas, y que su valor pudiera referenciar valores locales de una etiqueta script con scope
    Me pareció un poco extraña la idea de ver los atributos como configuración predeterminada
    Por ejemplo, si el atributo class solo mostrara el valor predeterminado, al alternar clases sería bastante raro que en el inspector DOM o en el HTML serializado no se viera una clase activa, o se viera una clase inactiva

    • No dije configuración predeterminada, sino simplemente configuración
      El atributo class funciona bien dentro de este esquema
      Yo, como dueño del documento, configuro el atributo class, y puedo cambiar esa configuración con el tiempo, pero eso lo hago solo yo
      En un esquema alternativo, el navegador agregaría o quitaría clases por su cuenta en respuesta a estados como hover o focus, pero por suerte no lo hace y se usan pseudoclases
 
superwoou 2024-04-27

Tradujeron tanto attribute como property como el mismo “atributo”...