Saltar al contenido

Binding fino: atributos, propiedades y booleanos

La diferencia entre atributos HTML y propiedades del DOM, cómo decide Solid por defecto, y los tres espacios de nombres que te dan el control total: attr: para forzar setAttribute, prop: para pasar valores JS vivos y objetos a web components, y bool: para la presencia de un atributo booleano.

Atributo no es propiedad

Un elemento del DOM es un objeto JavaScript con propiedades, y a la vez tiene atributos que son el reflejo textual de su HTML. A menudo se sincronizan, pero divergen de formas que importan:

  • El atributo value de un input es el valor inicial (lo que muestra el HTML y lo que se restaura en un reset); la propiedad value es el valor actual que el usuario edita. Escribir el atributo no cambia lo que el usuario ya escribió.
  • Los atributos solo pueden ser strings. La propiedad puede ser cualquier valor JS: un número, un array, un objeto, una función.
  • Algunos nombres difieren: la propiedad es className, el atributo es class; la propiedad es htmlFor, el atributo es for. Solid te deja escribir siempre class y for, más cómodos.
TSX
const el = document.querySelector("input")!;el.getAttribute("value");   // "" -> el atributo inicial del HTMLel.value = "hola";          // la PROPIEDAD viva cambiael.getAttribute("value");   // sigue siendo "" -> el atributo no se movio

Los alias son el azúcar de Solid sobre esta dualidad: escribes class y for, y el compilador asigna por debajo las propiedades className y htmlFor, más incómodas de teclear.

TSX
<label class="campo" for="correo">Correo</label>

Cómo elige Solid

Solid tiene una heurística: para un conjunto conocido de claves con semántica de propiedad (value, checked, selected, innerHTML, textContent…) asigna la propiedad; para el resto usa setAttribute. Esta elección por defecto acierta en la enorme mayoría de casos, y por eso escribes value={texto()} sin pensar y funciona como esperas.

TSX
// Solid sabe que value en un input es una propiedad: enlaza la propiedad viva.<input value={texto()} onInput={(e) => setTexto(e.currentTarget.value)} />// data-* y aria-* no son propiedades: van como atributos, correctamente.<div data-estado={estado()} aria-expanded={abierto() ? "true" : "false"} />

El problema aparece cuando la heurística no puede saber lo que quieres: elementos personalizados cuyas propiedades no conoce, casos donde necesitas el atributo aunque exista la propiedad, o al revés. Para eso están los tres espacios de nombres.

attr:, prop: y bool:

El caso que lo justifica todo es un web component. Sus propiedades ricas no caben en atributos (que son strings), y su estilo suele depender de la presencia de atributos booleanos:

TSX
// Un elemento personalizado que consume datos complejos y refleja estado.<mi-grafica  prop:puntos={serie()}          // un array real, no su toString()  prop:config={{ suave: true }}   // un objeto vivo por la propiedad  attr:tema={tema()}              // un string que el componente lee del atributo  bool:en-vivo={enVivo()}         // anade o quita el atributo en-vivo/>

Sin prop:, puntos={serie()} sobre un elemento desconocido acabaría como un atributo string —"[object Object]" en el peor caso—. Sin bool:, controlar la mera presencia de en-vivo requeriría lógica manual. Con estos espacios, el binding es declarativo y reactivo.

Del lado del componente esas propiedades existen de verdad; solo el atributo tema viaja como string:

TypeScript
// El web component: sus propiedades ricas NO son atributos.class MiGrafica extends HTMLElement {  set puntos(v: { x: number; y: number }[]) { /* redibuja con el array */ }  set config(v: { suave: boolean }) { /* aplica las opciones */ }  static observedAttributes = ["tema"];  attributeChangedCallback(_n: string, _viejo: string | null, valor: string) {    /* reacciona al string del atributo tema */  }}customElements.define("mi-grafica", MiGrafica);

Para nombres de atributo calculados en runtime no sirve attr: (su nombre es estático): usa el spread, que Solid aplica de forma reactiva.

TSX
const attrs = () => ({ "aria-label": etiqueta(), title: titulo() });<button {...attrs()} />;

Formularios: el ejemplo canónico

El par atributo/propiedad se ve nítido en los controles de formulario, donde existe la distinción entre el valor por defecto y el valor vivo:

TSX
// atributo value = valor por defecto: se ve en el HTML y vuelve en un reset.<input attr:value="borrador inicial" />// propiedad value = valor vivo y controlado: lo que el usuario ve ahora.<input prop:value={texto()} onInput={(e) => setTexto(e.currentTarget.value)} />

Lo mismo ocurre con checked frente al atributo checked (que es defaultChecked). Confundirlos produce el clásico bug de “el input no se deja controlar” o “el reset no restaura lo que espero”.