Montar Elm dentro de una app existente: Browser.element como widget en una página ajena
La adopción total de un lenguaje es la excepción; la adopción por incrustación es la norma. Esta lección estudia el peldaño de la escalera de programas que existe precisamente para eso: un constructor que produce un nodo del documento y renuncia explícitamente a controlar el título, la dirección y el resto de la página. Se examina el contrato exacto con el nodo anfitrión y la sorpresa habitual de que ese nodo se sustituya en lugar de rellenarse, la naturaleza de las banderas como único momento de configuración y su decodificación como frontera de validación, el papel de los puertos como única conversación posible con el código que rodea al widget, y los tres problemas que aparecen en cuanto la incrustación deja de ser un ejemplo y entra en una aplicación real: varias instancias en la misma página, la ausencia de una operación oficial de desmontaje, y la convivencia con código de terceros que modifica el documento por debajo. El objetivo es delimitar con precisión qué territorio pertenece a cada lado de la frontera y por qué esa delimitación tiene que ser explícita.
Casi nadie empieza un proyecto desde cero, y casi ningún lenguaje se adopta reescribiendo lo que ya funciona. La forma real en que una tecnología nueva entra en una organización es mucho más modesta y mucho más exigente: alguien identifica una pantalla concreta que duele, un formulario con quince reglas de validación cruzadas o un panel cuyo estado nadie consigue mantener coherente, y propone reescribir solo eso. El experimento tiene que convivir con lo que ya hay, compartir sesión y estilos con el resto de la página, y demostrar su valor antes de que nadie autorice el segundo paso. Browser.element es la respuesta de Elm a esa situación, y su virtud no está en lo que permite sino en lo que declara imposible. Un programa construido así puede pedir datos, escuchar el mundo y hablar con el código anfitrión, pero no puede tocar el título de la pestaña, no puede alterar el historial del navegador y no puede pintar fuera del nodo que se le asignó. La frontera no es una recomendación de estilo: está en el tipo, y por eso quien revisa el código sabe dónde termina el territorio de Elm sin leer una línea.
- Describir el contrato entre el programa incrustado y su nodo anfitrión, incluida la sustitución del nodo y sus consecuencias.
- Usar las banderas como único punto de configuración inicial y validar su forma en el borde en lugar de confiar en el anfitrión.
- Diseñar la conversación con el código circundante mediante puertos, distinguiendo lo que es un mensaje de lo que sería una llamada.
- Anticipar los problemas de varias instancias, de desmontaje y de convivencia con código que modifica el documento por fuera.
El contrato con el nodo anfitrión
El montaje consiste en dos líneas y una sorpresa. Se selecciona un nodo del documento, se pasa al constructor junto con las banderas y a partir de ese instante ese trozo del documento pertenece al entorno de ejecución de Elm. La sorpresa es que el nodo indicado no se rellena sino que se sustituye: el elemento que se entrega deja de existir y en su lugar aparece el árbol que produce la vista. La consecuencia práctica es que nunca debe entregarse un nodo que el resto de la página necesite conservar, ni uno con clases, identificadores o atributos de los que dependa una hoja de estilos o un guion externo. La costumbre correcta es crear un contenedor vacío dedicado, sin más papel que el de marcar el sitio, y entregar ese.
<section class="panel">
<h2>Reservas del mes</h2>
<div id="raiz-reservas"></div>
</section>
const raiz = document.getElementById("raiz-reservas");
const app = Elm.Reservas.init({
node: raiz,
flags: {
apiUrl: "/api/v2",
idioma: document.documentElement.lang,
usuario: window.SESION ? window.SESION.id : null,
},
});
Lo que ocurre dentro de esa región queda gobernado por el bucle de la arquitectura y por el algoritmo de comparación del árbol virtual, y lo que ocurre fuera sigue siendo responsabilidad de quien ya la tenía. La división es limpia mientras ambos lados la respeten, y toda la dificultad práctica de la incrustación consiste en que a veces no la respetan. Un guion antiguo que busca elementos por selector en todo el documento y les añade una clase, una biblioteca de ventanas emergentes que mueve nodos al final del cuerpo, una extensión del navegador que inyecta marcado dentro de un formulario: cualquiera de esas cosas modifica un árbol cuyo estado el entorno de ejecución de Elm cree conocer, y la siguiente actualización puede fallar porque el nodo que esperaba encontrar ya no está donde estaba.
Cuando una aplicación incrustada falla en producción con un error del árbol virtual, la causa casi nunca es un fallo del compilador ni del código propio: es que alguien modificó por fuera un nodo que estaba dentro de la región gobernada. Los sospechosos habituales son las bibliotecas que traducen la página al vuelo, los guiones de personalización y pruebas que reescriben marcado en cliente, los complementos de gestores de contraseñas que insertan elementos dentro de los formularios, y el código heredado que aún busca elementos por identificador. La mitigación no es defensiva sino topológica: elegir dónde va la frontera con cuidado, no incrustar dentro de zonas que otros guiones tocan, envolver las bibliotecas de terceros que manipulen el documento en elementos personalizados para que el árbol virtual las trate como cajas opacas, y en última instancia mover la frontera antes que negociarla.
Las banderas y los puertos: los dos únicos canales
Un programa incrustado recibe configuración una sola vez, al arrancar, a través de las banderas, y a partir de entonces todo lo demás viaja por puertos. Esta separación es más útil de lo que parece porque impone una distinción que en otros entornos se difumina: lo que es contexto inmutable de arranque, como la dirección base de la interfaz de datos, el idioma o el identificador de sesión, frente a lo que es conversación continua, como avisar de que el usuario guardó algo o recibir una notificación empujada desde el servidor. Las banderas se decodifican, lo cual significa que si el anfitrión envía un valor con forma equivocada el programa falla en el arranque con un mensaje concreto en lugar de arrastrar un dato corrupto hasta una pantalla lejana.
Aquí conviene una recomendación concreta sobre el tipo de las banderas. Declararlas con un registro tipado es cómodo y hace que el compilador imponga la forma, pero delega la validación en el mecanismo automático del entorno de ejecución, cuyo mensaje de fallo es correcto y poco informativo. Declararlas en cambio como un valor de datos sin interpretar y decodificarlas a mano dentro del arranque cuesta unas líneas más y devuelve el control: se pueden dar valores por defecto a lo que sea opcional, se puede transformar el dato de entrada en el tipo del dominio en el mismo paso, y sobre todo se puede decidir qué hacer cuando la configuración no llega, que en un widget incrustado casi nunca debe ser desaparecer sin explicación.
port module Reservas exposing (main)
import Browser
import Json.Decode as D
type alias Config =
{ apiUrl : String
, idioma : String
, usuario : Maybe String
}
port reservaGuardada : String -> Cmd msg
port notificacionEntrante : (D.Value -> msg) -> Sub msg
main : Program D.Value Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
Un contenedor propio
Un nodo vacío y dedicado, sin estilos ni identificadores de los que dependa nadie, porque será sustituido.
Banderas al arrancar
Todo el contexto inmutable llega una vez y se decodifica en el borde. Lo que no llega ahí, llega por puerto.
Puertos por instancia
Cada montaje tiene sus propios puertos. Dos widgets en la misma página no comparten canal ni se pisan.
Elementos personalizados
Un widget de JavaScript envuelto como elemento personalizado se convierte en una caja que el árbol virtual respeta.
Varias instancias, ciclo de vida y lo que no existe
En cuanto la incrustación funciona, aparece la petición de montar dos, y conviene saber que cada llamada al constructor produce una instancia independiente con su propio modelo y sus propios puertos. Suscribirse a un puerto de salida de una instancia no recibe los mensajes de la otra, y enviar por un puerto de entrada de una no alcanza a la otra. Eso es lo correcto y ahorra una clase entera de confusiones, pero obliga al anfitrión a llevar la cuenta de qué referencia corresponde a qué región de la página, y a no guardar la aplicación en una variable global como se hace en el ejemplo de una sola instancia.
const paneles = new Map();
for (const nodo of document.querySelectorAll(".panel-elm")) {
const app = Elm.Reservas.init({
node: nodo,
flags: { apiUrl: "/api/v2", idioma: "es", usuario: null },
});
app.ports.reservaGuardada.subscribe((carga) => {
document.dispatchEvent(new CustomEvent("reserva:guardada", { detail: carga }));
});
paneles.set(nodo.dataset.panelId, app);
}
Lo que no existe, y hay que decirlo con claridad porque es la ausencia que más sorprende, es una operación oficial de desmontaje. El objeto que devuelve el constructor no ofrece una función para detener la aplicación, cancelar sus suscripciones y devolver el nodo a su estado anterior. La práctica establecida en aplicaciones que montan y desmontan regiones dinámicamente consiste en retirar del documento el contenedor entero, eliminar todas las referencias a la aplicación para que el recolector pueda liberarla, y anular las suscripciones a sus puertos desde el lado del anfitrión. Es una solución de convención, no de tipo, y por tanto conviene concentrar esa lógica en una función única en lugar de repetirla en cada punto de montaje.
flowchart LR A[Pagina existente] --> B[Contenedor vacio dedicado] B --> C[init con nodo y banderas] C --> D[Region gobernada por Elm] D --> E[Puerto de salida hacia el anfitrion] A --> F[Puerto de entrada hacia Elm] F --> D D --> G[Fuera de la region manda el anfitrion] style D fill:#a6e3a1,color:#11111b style C fill:#cba6f7,color:#11111b style G fill:#89b4fa,color:#11111b
La estrategia de reescribir una aplicación por partes es vieja y casi siempre fracasa por la misma razón, y no es técnica. Fracasa porque la frontera entre lo nuevo y lo viejo se define en un documento, se comunica en una reunión y se erosiona un poco cada semana: alguien con prisa toca desde el lado nuevo un elemento del lado viejo porque era lo rápido, alguien del lado viejo lee una variable del nuevo porque estaba ahí, y a los ocho meses no queda frontera sino dos sistemas entrelazados que ahora hay que migrar juntos, que era exactamente lo que la estrategia pretendía evitar. Lo interesante de la incrustación con un constructor de capacidades limitadas es que la frontera deja de ser un acuerdo y pasa a ser una propiedad del código. El programa incrustado no puede tocar el título ni el historial porque no hay ninguna posición en su tipo donde eso quepa; no puede llamar a una función del anfitrión porque los puertos solo transportan datos y son asíncronos por construcción; no puede pintar fuera de su nodo porque el entorno de ejecución no le da acceso a ningún otro. Ninguna de esas restricciones depende de que alguien recuerde el acuerdo, y ahí está la diferencia entera. La generalización vale para cualquier migración incremental en cualquier lenguaje, y es que las fronteras que se documentan se disuelven mientras que las que se declaran en un tipo, un módulo con exportaciones mínimas o un proceso separado sobreviven a la rotación de personal, a la presión de las fechas y al olvido colectivo. Antes de empezar una migración por partes, la pregunta que decide el resultado no es por dónde empezamos sino qué impide técnicamente que esta frontera se cruce cuando nadie mire. Si la respuesta es la disciplina del equipo, la frontera ya está perdida y solo falta el tiempo.
- Monta un programa incrustado en una página existente y comprueba en el inspector que el nodo entregado fue sustituido, no rellenado.
- Pasa por banderas un valor con forma equivocada a propósito y observa dónde y cómo falla; razona qué habría ocurrido si no se decodificaran.
- Monta dos instancias en la misma página, suscríbete a los puertos de ambas y verifica que los mensajes no se cruzan.
- Escribe una función de desmontaje que retire el contenedor, anule las suscripciones y suelte la referencia, y explica qué queda vivo si se olvida cualquiera de los tres pasos.
- Introduce un guion externo que modifique un nodo dentro de la región gobernada y provoca el fallo del árbol virtual; después mueve la frontera para que sea imposible.
- Envuelve una biblioteca de JavaScript que manipule el documento en un elemento personalizado y argumenta por qué esa envoltura es preferible a llamarla desde un puerto.