Outlets de lista

A veces un outlet representa una región que contiene muchos ítems similares — una lista de monedas, un feed de tarjetas, una tabla de resultados de búsqueda. Plugster soporta esto directamente: cualquier outlet que declare data-child-templates se vuelve un outlet de lista, y el framework le adjunta una pequeña API para construir, leer, mover y remover ítems. Esta página es la referencia para esa API.

Declarar un outlet de lista #

Un outlet de lista es un outlet regular que también lleva el atributo data-child-templates. El valor del atributo es un array JSON de una o más URLs apuntando a archivos HTML estáticos — las plantillas de fila que el framework instanciará por ítem.

        
            <div data-controller-name="ExchangeRatesPlugster">
                <ul data-outlet-id="ratesList"
                    data-child-templates='["rate-row-template.html"]'></ul>
            </div>
        
    

La plantilla de fila es HTML plano. Usa data-child-outlet-id — no data-outlet-id — para declarar sus outlets internos:

        
            <!-- rate-row-template.html -->
            <li>
                <span data-child-outlet-id="currencyCodeLabel"></span>:
                <span data-child-outlet-id="valueLabel"></span>
            </li>
        
    

Dos restricciones importantes sobre las plantillas de fila:

  • Son estáticas. Plugster las descarga como HTML plano; el motor de plantillas del servidor no las procesa. Helpers como {{ _() }} o {{ render_snippet() }} no están disponibles dentro de una plantilla de fila. Los strings localizables dentro de los ítems de lista deben aplicarse desde el controlador vía translateTo, después de que el ítem se construye.
  • El framework las cachea. Cada URL de plantilla se descarga una vez por página y la respuesta se guarda en Plugster.childTemplateHtmlCache. Las peticiones concurrentes para la misma URL comparten un único jqXHR en vuelo vía Plugster.childTemplateRequestCache. Si varios Plugsters referencian la misma URL en sus data-child-templates, solo el primero dispara una petición de red.

Varias plantillas de fila #

Algunas listas necesitan más de una forma de fila — una fila "normal" y una fila "eliminada", por ejemplo. Pasa varias URLs en el array JSON y el controlador escoge cuál usar por ítem vía el primer argumento de buildListItem (el índice de plantilla):

        
            <ul data-outlet-id="ratesList"
                data-child-templates='["rate-row-normal.html", "rate-row-deleted.html"]'>
            </ul>
        
    

El índice de plantilla 0 usa la primera URL, el índice 1 usa la segunda, y así sucesivamente. Las formas pueden diferir libremente — el framework compilará los elementos internos data-child-outlet-id de cada plantilla independientemente.

buildListItem #

La firma completa:

        
            listOutlet.buildListItem(
                templateIndex,        // qué plantilla hija instanciar
                key,                  // identificador string único para el ítem
                data,                 // payload arbitrario guardado junto al ítem
                outletsSchema,        // mapa de nombre de child-outlet → configuración del outlet
                atIndex = 0,          // opcional, dónde insertar en la lista
                itemClickCallback     // opcional, se dispara cuando se hace clic en la fila
            );
        
    

Uso típico desde un controlador:

        
            invalidateRatesList(forCurrency) {
                let self = this;
                self._.selectedCurrencyLabel.text(forCurrency);

                self.exchangeRatesSvcs.getLatest(forCurrency).then(function (response) {
                    self._.ratesList.clear();
                    Object.keys(response.rates).forEach(function (currencyCode) {
                        let rate = response.rates[currencyCode];
                        let itemData = { currencyCode, rate };

                        let itemOutlets = self._.ratesList.buildListItem(
                            0,
                            currencyCode,
                            itemData,
                            { currencyCodeLabel: {}, valueLabel: {} }
                        );

                        if (!itemOutlets) return;

                        itemOutlets.currencyCodeLabel.text(currencyCode);
                        itemOutlets.valueLabel.text(rate);
                    });
                });
            }
        
    

buildListItem devuelve un objeto de outlets que refleja el schema que pasaste, más un outlet especial root que apunta al elemento de la fila misma. Los handles están scopeados a ese único ítem — direccionar itemOutlets.valueLabel solo toca el valueLabel de ese ítem, nunca el de otra fila.

Si la clave ya existe en la lista, buildListItem devuelve null en lugar de construir. Trata null como una señal para saltarte — típicamente con un return temprano como se muestra arriba — en lugar de como un error.

Posicionar ítems nuevos #

El argumento atIndex controla dónde aterriza el ítem nuevo. El default es 0 — es decir, los ítems nuevos van al inicio de la lista. Cuando insertas en una posición ya ocupada, cada ítem en o después de esa posición se desplaza hacia adelante en uno, y el index rastreado de cada ítem se actualiza acordemente.

        
            self._.ratesList.buildListItem(0, 'eur', eurData, schema);          // índice 0
            self._.ratesList.buildListItem(0, 'usd', usdData, schema, 1);      // índice 1, después de eur
            self._.ratesList.buildListItem(0, 'cop', copData, schema, 1);      // inserta en 1; usd se desplaza a 2
        
    

Leer ítems #

El outlet de lista expone una pequeña API de lectura para los ítems que contiene:

        
            self._.ratesList.count();                  // número de ítems actualmente en la lista
            self._.ratesList.getData('eur');           // { currencyCode: 'EUR', rate: ... } o null si falta
            self._.ratesList.getOutlets('eur');        // handle de outlets scopeados (incl. root) o null si falta
            self._.ratesList.getItems();               // diccionario clave-ítem — orden de iteración NO garantizado
            self._.ratesList.getItemsAsArray();        // array ordenado por index rastreado — úsalo cuando importa el orden
        
    

La distinción entre getItems() y getItemsAsArray() es la trampa que vale destacar: getItems() te devuelve un diccionario plano, y la iteración de diccionarios de JavaScript no garantiza coincidir con el orden visual en el DOM. Si tu código se preocupa por el orden de la lista — exportar ítems en el orden que aparecen, encontrar el ítem siguiente después de cierta clave, comparar contra otra lista ordenada — usa getItemsAsArray() en su lugar:

        
            self._.ratesList.getItemsAsArray().forEach((item) => {
                console.log(item.index, item.data, item.outlets);
            });
        
    

Actualizar y mover ítems #

setData(key, data) reemplaza los datos guardados de un ítem sin reconstruir la fila. El DOM no se toca — si los datos manejan estado visible, el controlador es responsable de re-renderizar los outlets de la fila que los reflejan.

        
            self._.ratesList.setData('eur', { currencyCode: 'EUR', rate: newRate });
            self._.ratesList.getOutlets('eur').valueLabel.text(newRate);
        
    

moveItem(key, direction) intercambia el ítem identificado por key con su vecino en currentIndex + direction — típicamente +1 o -1. El DOM y los índices rastreados se actualizan juntos, atómicamente. El método lanza excepción si la clave es desconocida o si la posición resultante caería fuera de [0, count).

        
            self._.ratesList.moveItem('cop', +1);      // intercambia cop con el ítem de abajo
            self._.ratesList.moveItem('cop', -1);      // intercambia cop con el ítem de arriba
        
    

Remover ítems #

        
            self._.ratesList.delete('eur');            // remueve un ítem por clave
            self._.ratesList.clear();                  // remueve todos los ítems
        
    

Ambos métodos desadjuntan los nodos del DOM y descartan el bookkeeping de los ítems que remueven. Los index rastreados de los ítems alrededor se renumeran para que getItemsAsArray() permanezca contiguo.

API en resumen #

        
            buildListItem(templateIndex, key, data, outletsSchema, atIndex?, itemClickCallback?)
                                                       → crea el ítem; devuelve outlets scopeados o null
            count()                                    → número de ítems
            setData(key, data)                         → reemplaza los datos de un ítem
            getData(key)                               → lee los datos de un ítem, o null
            getOutlets(key)                            → handle de outlets scopeados, o null
            getItems()                                 → diccionario clave-ítem (SIN ORDEN)
            getItemsAsArray()                          → array ordenado por index — úsalo cuando importa el orden DOM
            moveItem(key, direction)                   → intercambia con vecino en currentIndex + direction
            delete(key)                                → remueve un ítem
            clear()                                    → remueve todos los ítems
        
    

Cuándo usar outlets de lista #

Los outlets de lista son la herramienta correcta cuando los ítems en una región comparten estructura y el controlador gestiona su ciclo de vida individualmente (insertar, actualizar, reordenar, remover). No son la herramienta correcta para renderizado condicional puntual — si tienes dos o tres sub-regiones fijas que se muestran u ocultan según el estado, un outlet regular más show()/hide() es más simple.

Para listas que crecen y se reducen según los datos — el caso dominante — los outlets de lista son cómo el framework espera que trabajes, y la API de arriba es intencionalmente pequeña como para tenerla en la cabeza.

Content