wandres.dev
EVENTOS Y BINDING · on:, delegación, refs

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.

⏱ 13 min

Un <input> tiene un atributo value y una propiedad value, y no son lo mismo: el atributo es el valor inicial escrito en el HTML, la propiedad es el valor vivo que el usuario está tecleando ahora. La mayoría de las veces Solid elige bien por ti y no piensas en ello. Pero en cuanto tocas web components, formularios controlados, SVG o atributos ARIA, la distinción se vuelve la diferencia entre que algo funcione y que falle en silencio. Los espacios attr:, prop: y bool: son el bisturí.

🎯 Al terminar esta lección sabrás
  • Distinguir un atributo HTML (string, setAttribute) de una propiedad del DOM (valor JS vivo).
  • Entender que Solid elige propiedad o atributo por ti, y cuándo esa elección no basta.
  • Forzar el modo con attr: y prop:, y pasar objetos a web components con prop:.
  • Usar bool: para controlar la presencia de un atributo booleano en elementos arbitrarios.

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.
const el = document.querySelector("input")!;
el.getAttribute("value");   // "" -> el atributo inicial del HTML
el.value = "hola";          // la PROPIEDAD viva cambia
el.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.

<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.

// 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:

🏷️

attr: fuerza el atributo

attr:value={v()} llama a setAttribute pase lo que pase. Para el valor por defecto de un formulario, SVG, atributos personalizados con guiones, o cuando un web component lee de atributos.

⚙️

prop: fuerza la propiedad

prop:datos={obj()} asigna la propiedad JS directamente. Imprescindible para pasar objetos, arrays o funciones a un web component: los atributos solo serían strings.

🔘

bool: presencia booleana

bool:activo={cond()} añade el atributo cuando es true y lo quita cuando es false. Para presencia de atributos en elementos arbitrarios y custom elements.

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:

// 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:

// 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.

const attrs = () => ({ "aria-label": etiqueta(), title: titulo() });
<button {...attrs()} />;
📝
El reflejo es parcial y direccional

Algunos pares se sincronizan en ambos sentidos (id), otros solo del atributo a la propiedad al parsear el HTML (el value inicial), y otros no se reflejan en absoluto: una propiedad JS que fijas con prop: no aparece como atributo, y por eso no la ves como texto en el inspector. Entender que el reflejo es parcial explica por qué a veces cambias uno y el otro no se entera: son dos almacenes, no uno.

flowchart TD
D[Que quieres escribir en el elemento] --> A{Naturaleza del destino}
A -->|string visible en el HTML| ATTR[attr fuerza setAttribute]
A -->|valor JS vivo objeto o array| PROP[prop fuerza la propiedad]
A -->|presencia de un atributo booleano| BOOL[bool anade o quita el atributo]
A -->|caso conocido y normal| AUTO[Solid decide propiedad o atributo por ti]
style PROP fill:#a6e3a1,color:#11111b
style ATTR fill:#89b4fa,color:#11111b
style BOOL fill:#fab387,color:#11111b

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:

// 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”.

💡
attr: para SVG y ARIA dinámicos

En SVG muchos atributos no tienen propiedad equivalente, y algunos ARIA quieren el string exacto. Si un binding dinámico “no pinta” en un <svg> o un role/aria-* no reacciona, attr: fuerza el setAttribute correcto. Es el arreglo de referencia cuando la heurística de propiedad no aplica al espacio de nombres del elemento.

Dos mundos que el navegador mantiene casi sincronizados

La raíz de toda esta lección es una verdad del DOM anterior a cualquier framework: un elemento vive en dos planos, el textual de los atributos y el vivo de las propiedades, y el navegador los sincroniza solo a veces y solo en una dirección. Solid no oculta esa dualidad; te da una elección por defecto sensata y, cuando no basta, tres verbos para hablar con precisión con el DOM. attr: es “escribe esto en el HTML tal cual”, prop: es “asigna este valor JS a la propiedad”, bool: es “que este atributo exista o no según la condición”. Quien entiende esto deja de tener bugs fantasma con formularios controlados, puede integrar cualquier web component pasándole objetos por prop: en vez de pelearse con la serialización, y sabe por qué un binding funciona en un <div> pero no en un <svg>. La distinción atributo/propiedad no es trivia académica: es el contrato de bajo nivel entre tu código declarativo y el árbol imperativo que el navegador termina manipulando. Solid te da el control fino precisamente porque a este nivel el detalle decide si algo funciona.

⚔️ Domina los tres espacios
  1. Crea un <input> con attr:value y otro con prop:value controlado; teclea en ambos y observa cuál conserva el estado y cuál se restaura en un reset del formulario.
  2. Define un web component mínimo con una propiedad datos y pásale un array real con prop:datos; comprueba que llega como array y no como string.
  3. Usa bool: para alternar la presencia de un atributo personalizado y estilízalo desde CSS con un selector de atributo [en-vivo].
  4. Toma un atributo SVG dinámico que “no pinta” con el binding normal y arréglalo con attr:.
  5. Explica en una frase, para value en un input, por qué el atributo es el valor por defecto y la propiedad es el valor vivo.