Cómo crear una lista progresivamente mejorada, accesible, filtrable y paginada

 

 

 

  • Clase magistral de CSS moderno avanzado, con Manuel Matuzović
  • Anuncie en la revista Smashing

  • Índice
    1. Marcos ligeros
    2. alpino.js
      1. Una lista estática y paginada
      2. Configuración
      3. Agregar contenido
      4. Mostrando contenido
      5. Paginación
    3. Una lista dinámica paginada y filtrable
      1. Recuperacion de datos
      2. Paginación
      3. Filtering
      4. Bug Fix: Watching a Component Property

    ¿Alguna vez te has preguntado cómo crear una lista paginada que funcione con y sin JavaScript? En este artículo, Manuel explica cómo aprovechar el poder de la mejora progresiva y hacerlo con Eleventy y Alpine.js.

     

    La mayoría de los sitios que construyo son sitios estáticos con archivos HTML generados por un generador de sitios estáticos o páginas servidas en un servidor por un CMS como WordPress o CraftCMS . Utilizo JavaScript solo en la parte superior para mejorar la experiencia del usuario. Lo uso para cosas como widgets de divulgación, acordeones, navegaciones desplegables o modales.

    Los requisitos para la mayoría de estas características son simples, por lo que usar una biblioteca o un marco sería excesivo. Sin embargo, recientemente me encontré en una situación en la que escribir un componente desde cero en Vanilla JS sin la ayuda de un marco habría sido demasiado complicado y complicado.

    Marcos ligeros

    Mi tarea era agregar múltiples filtros, clasificación y paginación a una lista de elementos existente. No quería usar un marco de JavaScript como Vue o React, solo porque necesitaba ayuda en algunos lugares de mi sitio y no quería cambiar mi pila. Consulté Twitter y la gente sugirió marcos mínimos como lit , petite-vue , hyperscript , htmx o Alpine.js . Elegí Alpine porque parecía que era exactamente lo que estaba buscando:

    “Alpine es una herramienta minimalista y robusta para componer comportamientos directamente en su marcado. Piense en ello como jQuery para la web moderna. Introduce una etiqueta de script y ponte manos a la obra”.

     

    alpino.js

    Alpine es una colección liviana (~7 KB) de 15 atributos, 6 propiedades y 2 métodos. No entraré en los conceptos básicos (consulte este artículo sobre Alpine de Hugo Di Francesco o lea los documentos de Alpine ), pero permítame presentarle rápidamente Alpine:

    Nota: Puede omitir esta introducción e ir directamente al contenido principal del artículo si ya está familiarizado con Alpine.js.

    Digamos que queremos convertir una lista simple con muchos elementos en un widget de divulgación . Podrías usar los elementos HTML nativos: detalles y resumen para eso, pero para este ejercicio usaré Alpine.

    De forma predeterminada, con JavaScript deshabilitado, mostramos la lista, pero queremos ocultarla y permitir a los usuarios abrirla y cerrarla presionando un botón si JavaScript está habilitado:

    h2Beastie Boys Anthology/h2pThe Sounds of Science is the first anthology album by American rap rock group Beastie Boys composed of greatest hits, B-sides, and previously unreleased tracks./pol liBeastie Boys/li liSlow And Low/li liShake Your Rump/li liGratitude/li liSkills To Pay The Bills/li liRoot Down/li liBelieve Me/li …/ol

    Primero, incluimos Alpine usando una scriptetiqueta. Luego envolvemos la lista en a divy usamos la x-datadirectiva para pasar datos al componente. La openpropiedad dentro del objeto que pasamos está disponible para todos los hijos de div:

    div x-data="{ open: false }" ol liBeastie Boys/li liSlow And Low/li … /ol/divscript src="https://unpkg.com/[email protected]/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"/script

    Podemos usar la openpropiedad de la x-showdirectiva, que determina si un elemento es visible o no:

    div x-data="{ open: false }" ol x-show="open" liBeastie Boys/li liSlow And Low/li … /ol/div

    Como lo configuramos open, falsela lista ahora está oculta.

    A continuación, necesitamos un botón que alterne el valor de la openpropiedad. Podemos agregar eventos usando la x-on:clickdirectiva o la @-Syntax más corta @click:

    div x-data="{ open: false }" button @click="open = !open"Tracklist/button ol x-show="open" liBeastie Boys/li liSlow And Low/li … /ol/div

    Al presionar el botón, openahora se cambia entre falsey truey x-showse observan reactivamente estos cambios, mostrando y ocultando la lista en consecuencia.

    Si bien esto funciona para usuarios de teclado y mouse, es inútil para usuarios de lectores de pantalla, ya que necesitamos comunicar el estado de nuestro widget. Podemos hacerlo alternando el valor del aria-expandedatributo:

    button @click="open = !open" :aria-expanded="open" Tracklist/button

    También podemos crear una conexión semántica entre el botón y la lista usando lectores aria-controlsde pantalla que admitan el atributo :

    button @click="open = ! open" :aria-expanded="open" aria-controls="tracklist" Tracklist/buttonol x-show="open" …/ol

    Aquí está el resultado final:

     

    Consulte el Pen [widget de divulgación simple con Alpine.js] (https://codepen.io/smashingmag/pen/xxpdzNz) de Manuel Matuzovic .

    ¡Con buena pinta! Puede mejorar el contenido estático existente con JavaScript sin tener que escribir una sola línea de JS. Por supuesto, es posible que necesites escribir algo de JavaScript, especialmente si estás trabajando en componentes más complejos.

    Una lista estática y paginada

    Bien, ahora que conocemos los conceptos básicos de Alpine.js, diría que es hora de crear un componente más complejo.

    Nota : Puedes echar un vistazo al resultado final antes de comenzar.

    Quiero crear una lista paginada de mis discos de vinilo que funcione sin JavaScript. Usaremos el generador de sitios estáticos once (o abreviado “11ty”) para eso y Alpine.js para mejorarlo haciendo que la lista sea filtrable.

    ¿Alguien más aquí también es fanático de los discos de vinilo? 😉 ( Vista previa grande )

    Configuración

    Antes de comenzar, configuremos nuestro sitio. Nosotros necesitamos:

    • una carpeta de proyecto para nuestro sitio,
    • 11ty para generar archivos HTML,
    • un archivo de entrada para nuestro HTML,
    • un archivo de datos que contiene la lista de registros.

    En su línea de comando, navegue hasta la carpeta donde desea guardar el proyecto, cree una carpeta y cddentro de ella:

    cd Sites # or wherever you want to save the projectmkdir myrecordcollection # pick any namecd myrecordcollection

    Luego crea un package.jsonarchivo e instala once :

    npm init -ynpm install @11ty/eleventy

    A continuación, cree un index.njkarchivo ( .njksignifica que es un archivo Nunjucks; más información sobre esto a continuación) y una carpeta _datacon records.json:

    touch index.njkmkdir _datatouch _data/records.json

    No es necesario realizar todos estos pasos en la línea de comando. También puede crear carpetas y archivos en cualquier interfaz de usuario. La estructura final de archivos y carpetas se ve así:

    ( Vista previa grande )

    Agregar contenido

    11ty le permite escribir contenido directamente en un archivo HTML (o Markdown, Nunjucks y otros lenguajes de plantilla ). Incluso puede almacenar datos en la portada o en un archivo JSON. No quiero administrar cientos de entradas manualmente, así que las almacenaré en el archivo JSON que acabamos de crear. Agreguemos algunos datos al archivo:

    [ { "artist": "Akne Kid Joe", "title": "Die große Palmöllüge", "year": 2020 }, { "artist": "Bring me the Horizon", "title": "Post Human: Survial Horror", "year": 2020 }, { "artist": "Idles", "title": "Joy as an Act of Resistance", "year": 2018 }, { "artist": "Beastie Boys", "title": "Licensed to Ill", "year": 1986 }, { "artist": "Beastie Boys", "title": "Paul's Boutique", "year": 1989 }, { "artist": "Beastie Boys", "title": "Check Your Head", "year": 1992 }, { "artist": "Beastie Boys", "title": "Ill Communication", "year": 1994 }]

    Finalmente, agreguemos una estructura HTML básica al index.njkarchivo y comencemos once:

     

    !DOCTYPE htmlhtmlhead meta charset="UTF-8" meta name="viewport" content="width=device-width, initial-scale=1.0" titleMy Record Collection/title/headbody h1My Record Collection/h1 /body/html

    Al ejecutar el siguiente comando, debería poder acceder al sitio en https://localhost:8080:

    eleventy --serve

    Once navegando en puerto :8080. El sitio simplemente muestra el título "Mi colección de discos". ( Vista previa grande )

    Mostrando contenido

    Ahora tomemos los datos de nuestro archivo JSON y convirtámoslos en HTML. Podemos acceder a él recorriendo el recordsobjeto en nunjucks:

    div ol {% for record in records %} li strong{{ record.title }}/strongbr Released in time datetime="{{ record.year }}"{{ record.year }}/time by {{ record.artist }}. /li {% endfor %} /ol/div

    7 discos listados, cada uno con su título, artista y fecha de lanzamiento. ( Vista previa grande )

    Paginación

    Eleventy admite la paginación lista para usar. Todo lo que tenemos que hacer es agregar un bloque frontal a nuestra página, decirle a 11ty qué conjunto de datos debe usar para la paginación y, finalmente, tenemos que adaptar nuestro forbucle para usar la lista paginada en lugar de todos los registros:

    ---pagination: data: records size: 5---!DOCTYPE htmlhtml head meta charset="UTF-8" meta name="viewport" content="width=device-width, initial-scale=1.0" titleMy Record Collection/title /head body h1My Record Collection/h1 div pShowing output{{ records.length }} records/output/p div aria-labelledby="message" role="region" ol {% for record in pagination.items %} li strong{{ record.title }}/strongbr Released in time datetime="{{ record.year }}"{{ record.year }}/time by {{ record.artist }}. /li {% endfor %} /ol /div /div /body/html

    Si accede nuevamente a la página, la lista solo contiene 5 elementos. También puedes ver que agregué un mensaje de estado (ignora el outputelemento por ahora), envolví la lista en a divcon la role"región" y la etiqueté creando una referencia al #messageuso de aria-labelledby. Lo hice para convertirlo en un punto de referencia y permitir a los usuarios de lectores de pantalla acceder a la lista de resultados directamente mediante atajos de teclado.

    A continuación, agregaremos una navegación con enlaces a todas las páginas creadas por el generador de sitios estáticos. El paginationobjeto contiene un arrayque contiene todas las páginas. Usamos aria-current="page"para resaltar la página actual:

    nav aria-label="Select a page" ol {% for page_entry in pagination.pages %} {%- set page_url = pagination.hrefs[loop.index0] -%} li a href="{{ page_url }}"{% if page.url == page_url %} aria-current="page"{% endif %} Page {{ loop.index }} /a /li {% endfor %} /ol/nav

    Finalmente, agreguemos algo de CSS básico para mejorar el estilo:

     

    body { font-family: sans-serif; line-height: 1.5;}ol { list-style: none; margin: 0; padding: 0;}.records * + * { margin-top: 2rem;}h2 { margin-bottom: 0;}nav { margin-top: 1.5rem;}.pages { display: flex; flex-wrap: wrap; gap: 0.5rem;}.pages a { border: 1px solid #000000; padding: 0.5rem; border-radius: 5px; display: flex; text-decoration: none;}.pages a:where([aria-current]) { background-color: #000000; color: #ffffff;}.pages a:where(:focus, :hover) { background-color: #6c6c6c; color: #ffffff;}

    ( Vista previa grande )

    Puede verlo en acción en la demostración en vivo y consultar el código en GitHub .

    Esto funciona bastante bien con 7 registros. Puede que incluso funcione con 10, 20 o 50, pero tengo más de 400 registros. Podemos facilitar la navegación por la lista agregando filtros.

    Una lista dinámica paginada y filtrable

    Me gusta JavaScript, pero también creo que el contenido principal y la funcionalidad de un sitio web deberían ser accesibles sin él. Esto no significa que no pueda usar JavaScript en absoluto, simplemente significa que comienza con una base básica renderizada por el servidor de su componente o sitio, y agrega funcionalidad capa por capa. Esto se llama mejora progresiva .

    Nuestra base en este ejemplo es la lista estática creada con 11ty y ahora agregamos una capa de funcionalidad con Alpine.

    Primero, justo antes de la bodyetiqueta de cierre, hacemos referencia a la última versión (al momento de escribir 3.9.1) de Alpine.js:

     script src="https://unpkg.com/[email protected]/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"/script/body

    Nota: tenga cuidado al utilizar una CDN de terceros, esto puede tener todo tipo de implicaciones negativas (rendimiento, privacidad, seguridad). Considere hacer referencia al archivo localmente o importarlo como un módulo .
    En caso de que se pregunte por qué no ve el hash de integridad de subrecursos en los documentos oficiales, es porque lo creé y agregué manualmente. Recetas para Cookeo

    Dado que nos estamos moviendo hacia el mundo de JavaScript, debemos poner nuestros registros a disposición de Alpine.js. Probablemente no sea la mejor, pero la solución más rápida es crear un .eleventy.jsarchivo en su carpeta raíz y agregar las siguientes líneas:

    module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy("_data");};

    Esto garantiza que eleventy no solo genere archivos HTML, sino que también copie el contenido de la _datacarpeta en nuestra carpeta de destino, haciéndola accesible para nuestros scripts.

     

    Recuperacion de datos

    Al igual que en el ejemplo anterior, agregaremos la x-datadirectiva a nuestro componente para pasar datos:

    div x-data="{ records: [] }"/div

    No tenemos ningún dato, por lo que debemos recuperarlo cuando se inicializa el componente. La x-initdirectiva nos permite conectarnos a la fase de inicialización de cualquier elemento y realizar tareas:

    div x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }" div x-text="records"/div […]/div

    Si generamos los resultados directamente, veremos una lista de [object Object]correos electrónicos, porque estamos buscando y recibiendo un archivo array. En su lugar, deberíamos iterar sobre la lista usando la x-fordirectiva en una templateetiqueta y generar los datos usando x-text:

    template x-for="record in records" li strong x-text="record.title"/strongbr Released in time :datetime="record.year" x-text="record.year"/time by span x-text="record.artist"/span. /li/template

    El templateelemento HTML es un mecanismo para contener HTML que no debe representarse inmediatamente cuando se carga una página, pero que se puede crear una instancia posteriormente durante el tiempo de ejecución utilizando JavaScript.

    MDN:: templateEl elemento de plantilla de contenido

    Así es como se ve la lista completa ahora:

    div x-init="records = await (await fetch('/_data/records.json')).json()" x-data="{ records: [] }" pShowing output{{ records.length }} records/output/p div aria-labelledby="message" role="region" ol template x-for="record in records" li strong x-text="record.title"/strongbr Released in time :datetime="record.year" x-text="record.year"/time by span x-text="record.artist"/span. /li /template {%- for record in pagination.items %} li strong{{ record.title }}/strongbr Released in time datetime="{{ record.year }}"{{ record.year }}/time by {{ record.artist }}. /li {%- endfor %} /ol /div […]/div

    ¿No es sorprendente lo rápido que pudimos recuperar y generar datos? Consulte la demostración a continuación para ver cómo Alpine completa la lista con resultados.

    Sugerencia: No ve ningún código Nunjucks en este CodePen porque 11ty no se ejecuta en el navegador. Acabo de copiar y pegar el HTML renderizado de la primera página.

    Consulte el lápiz [Paginación + filtro con Alpine.js Paso 1] (https://codepen.io/smashingmag/pen/abEWRMY) de Manuel Matuzovic .

    Puede lograr mucho utilizando las directivas de Alpine, pero en algún momento depender sólo de los atributos puede resultar complicado. Es por eso que decidí mover los datos y parte de la lógica a un objeto componente Alpine separado.

    Así es como funciona: en lugar de pasar datos directamente, ahora hacemos referencia a un componente usando x-data. El resto es prácticamente idéntico: defina una variable para contener nuestros datos y luego obtenga nuestro archivo JSON en la fase de inicialización. Sin embargo, no hacemos eso dentro de un atributo, sino dentro de una scriptetiqueta o archivo:

     

    div x-data="collection" […]/div[…]script document.addEventListener('alpine:init', () = { Alpine.data('collection', () = ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); }, init() { this.getRecords(); } })) })/scriptscript src="https://unpkg.com/[email protected]/dist/cdn.min.js" integrity="sha384-mDHH3kdyMS0F6QcfHCxEgPMMjssTurzucc7Jct3g1GOfB4p7PxJuugPP1NOLvE7I" crossorigin="anonymous"/script

    Al observar el CodePen anterior, probablemente haya notado que ahora tenemos un conjunto de datos duplicado. Esto se debe a que nuestra lista estática de 11ty todavía está ahí. Alpine tiene una directiva que le dice que ignore ciertos elementos DOM. No sé si esto es realmente necesario aquí, pero es una buena forma de marcar estos elementos no deseados . Entonces, agregamos la x-ignoredirectiva en nuestros 11 elementos de la lista, y agregamos una clase al htmlelemento cuando los datos se han cargado y luego usamos la clase y el atributo para ocultar esos elementos de la lista en CSS:

    style .alpine [x-ignore] { display: none; }/style[…]{%- for record in pagination.items %} li x-ignore strong{{ record.title }}/strongbr Released in time datetime="{{ record.year }}"{{ record.year }}/time by {{ record.artist }}. /li{%- endfor %}[…]script document.addEventListener('alpine:init', () = { Alpine.data('collection', () = ({ records: [], async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } })) })/script

    Hay 11 datos ocultos, los resultados provienen de Alpine, pero la paginación no funciona en este momento:

    Vea el lápiz [Paginación + Filtro con Alpine.js Paso 2] (https://codepen.io/smashingmag/pen/eYyWQOe) de Manuel Matuzovic .

    Paginación

    Antes de agregar filtros, paginaremos nuestros datos. 11ty nos hizo el favor de manejar toda la lógica por nosotros, pero ahora tenemos que hacerlo nosotros solos. Para dividir nuestros datos en varias páginas, necesitamos lo siguiente:

    • el número de elementos por página ( itemsPerPage),
    • la página actual ( currentPage),
    • el número total de páginas ( numOfPages),
    • un subconjunto dinámico y paginado de todos los datos ( page).
    document.addEventListener('alpine:init', () = { Alpine.data('collection', () = ({ records: [], itemsPerPage: 5, currentPage: 0, numOfPages: // total number of pages, page: // paged items async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))})

    El número de elementos por página es un valor fijo (5)y la página actual comienza con 0. Obtenemos el número de páginas dividiendo el número total de elementos por el número de elementos por página:

    numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) // 7 / 5 = 1.4 // Math.ceil(7 / 5) = 2},

    La forma más fácil para mí de obtener los elementos por página fue usar el slice()método en JavaScript y extraer la porción del conjunto de datos que necesito para la página actual:

     

    page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) // this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage // Page 1: 0 * 5, (0 + 1) * 5 (= slice(0, 5);) // Page 2: 1 * 5, (1 + 1) * 5 (= slice(5, 10);) // Page 3: 2 * 5, (2 + 1) * 5 (= slice(10, 15);)}

    Para mostrar solo los elementos de la página actual, tenemos que adaptar el forbucle para iterar pageen lugar de records:

    ol template x-for="record in page" li strong x-text="record.title"/strongbr Released in time :datetime="record.year" x-text="record.year"/time by span x-text="record.artist"/span. /li /template/ol

    We now have a page, but no links that allow us to jump from page to page. Just like earlier, we use the template element and the x-for directive to display our page links:

    ol template x-for="idx in numOfPages" li a :href="`/${idx}`" x-text="`Page ${idx}`" :aria-current="idx === currentPage + 1 ? 'page' : false" @click.prevent="currentPage = idx - 1"/a /li /template {% for page_entry in pagination.pages %} li x-ignore […] /li {% endfor %}/ol

    Since we don’t want to reload the whole page anymore, we put a click event on each link, prevent the default click behavior, and change the current page number on click:

    a href="/" @click.prevent="currentPage = idx - 1"/a

    Here’s what that looks like in the browser. (I’ve added more entries to the JSON file. You can download it on GitHub.)

    See the Pen [Pagination + Filter with Alpine.js Step 3](https://codepen.io/smashingmag/pen/GRymwjg) by Manuel Matuzovic.

    Filtering

    I want to be able to filter the list by artist and by decade.

    We add two select elements wrapped in a fieldset to our component, and we put a x-model directive on each of them. x-model allows us to bind the value of an input element to Alpine data:

    fieldset legendFilter by/legend label for="artist"Artist/label select x-model="filters.artist" option value=""All/option /select label for="decade"Decade/label select x-model="filters.year" option value=""All/option /select/fieldset

    Of course, we also have to create these data fields in our Alpine component:

    document.addEventListener('alpine:init', () = { Alpine.data('collection', () = ({ filters: { year: '', artist: '', }, records: [], itemsPerPage: 5, currentPage: 0, numOfPages() { return Math.ceil(this.records.length / this.itemsPerPage) }, page() { return this.records.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage) }, async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); document.documentElement.classList.add('alpine'); }, init() { this.getRecords(); } }))})

    If we change the selected value in each select, filters.artist and filters.year will update automatically. You can try it here with some dummy data I’ve added manually:

     

    See the Pen [Pagination + Filter with Alpine.js Step 4](https://codepen.io/smashingmag/pen/GGRymwEp) by Manuel Matuzovic.

    Now we have select elements, and we’ve bound the data to our component. The next step is to populate each select dynamically with artists and decades respectively. For that we take our records array and manipulate the data a bit:

    document.addEventListener('alpine:init', () = { Alpine.data('collection', () = ({ artists: [], decades: [], // […] async getRecords() { this.records = await (await fetch('/_data/records.json')).json(); this.artists = [...new Set(this.records.map(record = record.artist))].sort(); this.decades = [...new Set(this.records.map(record = record.year.toString().slice(0, -1)))].sort(); document.documentElement.classList.add('alpine'); }, // […] }))})

    This looks wild, and I’m sure that I’ll forget what’s going on here real soon, but what this code does is that it takes the array of objects and turns it into an array of strings (map()), it makes sure that each entry is unique (that’s what [...new Set()] does here) and sorts the array alphabetically (sort()). For the decade’s array, I’m additionally slicing off the last digit of the year because I don’t want this filter to be too granular. Filtering by decade is good enough.

    Next, we populate the artist and decade select elements, again using the template element and the x-for directive:

    label for="artist"Artist/labelselect x-model="filters.artist" option value=""All/option template x-for="artist in artists" option x-text="artist"/option /template/selectlabel for="decade"Decade/labelselect x-model="filters.year" option value=""All/option template x-for="year in decades" option :value="year" x-text="`${year}0`"/option /template/select

    Try it yourself in demo 5 on Codepen.

    See the Pen [Pagination + Filter with Alpine.js Step 5](https://codepen.io/smashingmag/pen/OJzmaZb) by Manuel Matuzovic.

    We’ve successfully populated the select elements with data from our JSON file. To finally filter the data, we go through all records, we check whether a filter is set. If that’s the case, we check that the respective field of the record corresponds to the selected value of the filter. If not, we filter this record out. We’re left with a filtered array that matches the criteria:

    get filteredRecords() { const filtered = this.records.filter((item) = { for (var key in this.filters) { if (this.filters[key] === '') { continue } if(!String(item[key]).includes(this.filters[key])) { return false } } return true }); return filtered}

    For this to take effect we have to adapt our numOfPages() and page() functions to use only the filtered records:

    numOfPages() { return Math.ceil(this.filteredRecords.length / this.itemsPerPage)},page() { return this.filteredRecords.slice(this.currentPage * this.itemsPerPage, (this.currentPage + 1) * this.itemsPerPage)},

    See the Pen [Pagination + Filter with Alpine.js Step 6](https://codepen.io/smashingmag/pen/GRymwQZ) by Manuel Matuzovic.

    Three things left to do:

    1. fix a bug;
    2. hide the form;
    3. update the status message.

    Bug Fix: Watching a Component Property

    When you open the first page, click on page 6, then select “1990” — you don’t see any results.






    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Cómo crear una lista progresivamente mejorada, accesible, filtrable y paginada

    Cómo crear una lista progresivamente mejorada, accesible, filtrable y paginada

    Clase magistral de CSS moderno avanzado, con Manuel Matuzović Anuncie en la revista Smashing Índice Marc

    programar

    es

    https://aprendeprogramando.es/static/images/programar-como-crear-una-lista-progresivamente-mejorada-1135-0.jpg

    2024-04-04

     

    Cómo crear una lista progresivamente mejorada, accesible, filtrable y paginada
    Cómo crear una lista progresivamente mejorada, accesible, filtrable y paginada

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20