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
valuede un input es el valor inicial (lo que muestra el HTML y lo que se restaura en unreset); la propiedadvaluees 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 esclass; la propiedad eshtmlFor, el atributo esfor. Solid te deja escribir siempreclassyfor, más cómodos.
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 movioLos 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:
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()} />;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”.