Más componentes de @nexa/ui: Table, Tabs, Dropdown y un buscador reactivo
Nueve componentes puramente visuales, y seis con estado real en cliente — sin que ninguno de estos últimos necesite una isla (Paso 9). Todos los ejemplos de este capítulo son funcionales tal cual están escritos: cada uno se probó en un navegador real antes de escribirse acá.
Table, Badge y Avatar
<div class="nx-table-wrap">
<table class="nx-table nx-table-zebra">
<thead><tr><th>Cliente</th><th>Estado</th></tr></thead>
<tbody>
<tr>
<td><span class="nx-avatar nx-avatar-sm">HM</span> Hector M.</td>
<td><span class="nx-badge nx-badge-success">Pagado</span></td>
</tr>
<tr>
<td><span class="nx-avatar nx-avatar-sm">AR</span> Ana R.</td>
<td><span class="nx-badge nx-badge-warning">Pendiente</span></td>
</tr>
</tbody>
</table>
</div>nx-table-wrap agrega scroll horizontal en pantallas chicas; nx-table-zebra alterna el fondo de las filas. nx-badge y nx-avatar son solo CSS — ninguno de los tres necesita JavaScript todavía (eso viene en la sección de ordenar/filtrar, más abajo).
Breadcrumbs, Alert y Divider
<nav class="nx-breadcrumbs" aria-label="breadcrumb">
<a class="nx-breadcrumbs-link" href="/">Inicio</a>
<a class="nx-breadcrumbs-link" href="/pedidos">Pedidos</a>
<span class="nx-breadcrumbs-link" aria-current="page">#1024</span>
</nav>
<div class="nx-alert nx-alert-warning">
<p class="nx-alert-title">Stock bajo</p>
<p>Quedan 3 unidades.</p>
</div>
<hr class="nx-divider" />nx-breadcrumbs pone el "/" entre pasos solo con CSS (::after) — no hace falta escribirlo a mano. aria-current="page" marca cuál es el paso actual. nx-alert tiene tres variantes de color (success/warning/danger) vía el borde izquierdo.
Tooltip, Progress y Skeleton
<span class="nx-tooltip">
Pasá el mouse acá
<span class="nx-tooltip-content">Info extra, sin JavaScript</span>
</span>
<div class="nx-progress">
<div class="nx-progress-bar" style={`width: ${data.percent}%`}></div>
</div>
<div class="nx-skeleton" style="width: 200px; height: 1rem;"></div>nx-tooltip-content se muestra con :hover/:focus-within — accesible con teclado, sin una línea de JS. nx-progress-bar necesita su width puesto a mano (fijo, o dinámico con data.*). nx-skeleton es un placeholder animado mientras algo carga.
Tabs, sin isla
function selectTab(event) {
ui.selectTab(event);
}
<div class="nx-tab-group">
<div class="nx-tab-list" role="tablist">
<button class="nx-tab" data-tab="uno" aria-selected="true" onClick={selectTab}>Uno</button>
<button class="nx-tab" data-tab="dos" onClick={selectTab}>Dos</button>
</div>
<div class="nx-tab-panel" data-tab="uno">Contenido uno</div>
<div class="nx-tab-panel" data-tab="dos" hidden>Contenido dos</div>
</div>ui.selectTab(event) lee de qué botón vino el click (event.currentTarget) y su data-tab, encuentra el .nx-tab-group más cercano, y hace el resto — no hace falta pasarle ningún elemento a mano. La misma función selectTab de una línea sirve para todos los botones de todos los grupos de pestañas de la página.
Accordion, con modo exclusivo opcional
function toggle(event) {
ui.toggleAccordionItem(event);
}
<div class="nx-accordion" data-exclusive="true">
<div class="nx-accordion-item">
<button class="nx-accordion-trigger" aria-expanded="false" onClick={toggle}>¿Envían a todo el país?</button>
<div class="nx-accordion-panel" hidden>Sí, a todo el país.</div>
</div>
</div>Por defecto varios items pueden estar abiertos a la vez. Con data-exclusive="true" en el .nx-accordion contenedor, abrir uno cierra los demás — útil para un FAQ donde solo tiene sentido leer una respuesta a la vez.
Dropdown, cerrado con click afuera o Escape
function toggleMenu(event) {
ui.toggleDropdown(event);
}
function pickOption(event) {
ui.selectDropdownOption(event);
}
<div class="nx-dropdown">
<button class="nx-dropdown-trigger" aria-haspopup="listbox" aria-expanded="false" onClick={toggleMenu}>
Elegir talle…
</button>
<ul class="nx-dropdown-menu" role="listbox" hidden>
<li class="nx-dropdown-option" role="option" data-value="s" onClick={pickOption}>S</li>
<li class="nx-dropdown-option" role="option" data-value="m" onClick={pickOption}>M</li>
<li class="nx-dropdown-option" role="option" data-value="l" onClick={pickOption}>L</li>
</ul>
</div>Igual que Dialog (Paso 7), pero sin atrapar el foco — un dropdown no bloquea el resto de la página. Elegir una opción actualiza el texto del trigger, marca aria-selected en la opción elegida, y cierra el menú solo.
Un buscador dentro de una tabla (y ordenarla por columna)
function filterOrders(event) {
ui.filterTable(document.getElementById("orders"), event.target.value);
}
function sortByColumn(event) {
ui.sortTable(event);
}
<input type="search" onInput={filterOrders} placeholder="Buscar pedido…" />
<table id="orders" class="nx-table">
<thead>
<tr>
<th data-sortable onClick={sortByColumn}>Cliente</th>
<th data-sortable onClick={sortByColumn}>Total</th>
</tr>
</thead>
<tbody>
<tr><td>Beta</td><td>30</td></tr>
<tr><td>Alfa</td><td>10</td></tr>
<tr><td>Gama</td><td>20</td></tr>
</tbody>
</table>ui.filterTable(table, query) oculta filas de tbody cuyo texto no matchea — es el buscador "dentro de la tabla". ui.sortTable(event) ordena por la columna del <th> clickeado, alternando ascendente/descendente, y compara como número si ambas celdas lo son (10 antes que 20, no "10" antes que "2" por orden de texto). Las dos operan client-side, sobre filas que el servidor ya renderizó — sin volver a pedir nada al backend, y sin ninguna isla.
Un buscador de página completa, como componente reactivo
function onSearchInput(event) {
const input = event.currentTarget;
input.pageSearch ??= ui.createPageSearch(document.getElementById("catalogo"));
input.pageSearch.setQuery(event.target.value);
}
<input type="search" onInput={onSearchInput} placeholder="Buscar en la página…" />
<div id="catalogo">
<article data-searchable>Zapatillas Nezha</article>
<article data-searchable>Camiseta Jade</article>
<article data-searchable>Gorra roja</article>
</div>ui.createPageSearch(container) filtra cualquier elemento data-searchable dentro de container, con un Signal real de @nexa/reactivity por debajo (el mismo Paso 4 de este tutorial) — no una simulación. Ojo con un detalle real: el chunk que Nexa extrae de onSearchInput es un módulo aislado, así que no puede depender de una variable declarada aparte en la página (un let a nivel de archivo no viaja con el chunk). Por eso el controlador se cuelga del propio <input> (input.pageSearch), que sí persiste entre una tecleada y la siguiente porque es el mismo elemento del DOM.
¿Isla, o función de ui? Cómo decidir
Si el estado es local a un widget concreto y se puede modelar como "manipular directamente el DOM que ya está en la página" (abrir/cerrar, marcar una pestaña activa, ordenar/filtrar filas ya renderizadas), es una función de ui + onClick/onInput — sin Virtual DOM, sin re-render, activación progresiva de siempre. Una isla (Paso 9) queda reservada para cuando de verdad hace falta un framework completo con su propio ciclo de vida (Vue, React) o composición real con props/children profundos — nunca como opción por defecto para "este botón hace algo". Los seis componentes de este capítulo (Tabs, Accordion, Dropdown, orden/filtro de Table, y el buscador de página) son la prueba de que se puede llegar bastante lejos sin abrir esa puerta.