Saltar al contenido

Directivas use: clickOutside y tooltip

Qué compila use:directiva, cómo escribir directivas personalizadas como clickOutside y tooltip con accesor reactivo y limpieza con onCleanup, por qué el nombre no debe tree-shakearse, y cómo tiparlas augmentando JSX.Directives para que TypeScript las reconozca.

Qué es una directiva

El compilador transforma use:foo={bar} en una llamada: foo(elemento, () => bar). Es decir, una directiva es una función que recibe el nodo del DOM y un accesor —una función que al invocarla devuelve el valor actual del binding—. Se ejecuta cuando el elemento se crea, dentro del ámbito reactivo del componente, lo que significa que puede registrar efectos y limpieza que seguirán el ciclo de vida del nodo.

TSX
import { onCleanup } from "solid-js";import type { Accessor } from "solid-js";// Firma canonica: (elemento, accesor del valor).function autofocus(el: HTMLInputElement, value: Accessor<boolean>) {  if (value()) el.focus();}// Uso: el compilador emite autofocus(input, () => true).<input use:autofocus={true} />;

Como el accesor es reactivo, si lo lees dentro de un createEffect la directiva reacciona a los cambios del valor. Y como corre dentro del owner del componente, un onCleanup dentro de ella se dispara cuando el elemento se destruye: por eso las directivas son el hogar natural de listeners que hay que quitar.

clickOutside: la directiva de referencia

El caso de uso clásico: cerrar un menú o modal cuando el usuario hace click fuera. La directiva engancha un listener al documento y lo retira al limpiar.

TSX
import { onCleanup } from "solid-js";import type { Accessor } from "solid-js";export function clickOutside(el: HTMLElement, accessor: Accessor<() => void>) {  const onClick = (e: MouseEvent) => {    // Si el click NO cayo dentro del elemento, invoca el callback del usuario.    if (!el.contains(e.target as Node)) accessor()?.();  };  document.body.addEventListener("click", onClick);  onCleanup(() => document.body.removeEventListener("click", onClick));}// Uso: cerrar el panel al hacer click fuera.function Menu() {  const [abierto, setAbierto] = createSignal(true);  return (    <Show when={abierto()}>      <div use:clickOutside={() => setAbierto(false)} class="panel">        contenido del menu      </div>    </Show>  );}

El onCleanup es lo que hace correcta a esta directiva: cuando <Show> desmonta el panel, el listener del documento se retira solo. Sin él, tendrías un listener huérfano por cada apertura, un fuga clásica.

tooltip: estado, reactividad y limpieza

Una directiva puede mantener estado interno y reaccionar al accesor. Un tooltip que muestra un texto al pasar el ratón, y que se actualiza si el texto cambia:

TSX
import { createEffect, onCleanup } from "solid-js";import type { Accessor } from "solid-js";export function tooltip(el: HTMLElement, accessor: Accessor<string>) {  let burbuja: HTMLDivElement | undefined;  const mostrar = () => {    burbuja = document.createElement("div");    burbuja.className = "tooltip";    burbuja.textContent = accessor();    const r = el.getBoundingClientRect();    burbuja.style.left = `${r.left}px`;    burbuja.style.top = `${r.bottom + 6}px`;    document.body.append(burbuja);  };  const ocultar = () => {    burbuja?.remove();    burbuja = undefined;  };  el.addEventListener("mouseenter", mostrar);  el.addEventListener("mouseleave", ocultar);  // Reactivo: si el texto cambia mientras el tooltip esta visible, se actualiza.  createEffect(() => {    const texto = accessor();    if (burbuja) burbuja.textContent = texto;  });  onCleanup(() => {    el.removeEventListener("mouseenter", mostrar);    el.removeEventListener("mouseleave", ocultar);    ocultar();  });}// Uso:<button use:tooltip={etiqueta()}>Ayuda</button>;

Tipar directivas y el problema del tree-shaking

Dos escollos que rara vez se explican bien. Primero, TypeScript no sabe que use:tooltip={string} es válido a menos que augmentes JSX.Directives, un mapa de nombre de directiva a tipo del valor del accesor:

TSX
declare module "solid-js" {  namespace JSX {    interface Directives {      clickOutside: () => void;   // el tipo que espera use:clickOutside={...}      tooltip: string;             // use:tooltip espera un string    }  }}

Esta augmentación debe vivir en un módulo (un archivo con import/export); si el archivo no tiene ninguno, añade export {} para forzarlo. Con ella, use:tooltip={42} da error de tipos y obtienes autocompletado.

Segundo, el tree-shaking. El nombre de la directiva solo aparece en la posición use:foo, que el minificador no reconoce como un uso real de la variable, así que puede borrar el import por “no usado”. Solid lo tiene en cuenta y el plugin de compilación marca el nombre, pero con imports desde otro módulo asegúrate de que no se elimine.