Un menú desplegable de búsqueda más inteligente con HTMX (Español (Spanish))

Un menú desplegable de búsqueda más inteligente con HTMX

Comments

NOTE: Apart from English (and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.

Monday, 16 September 2024

//

Less than a minute

Introducción

En un post anterior te mostré cómo crear un menú desplegable con Alpine.js y HTMX entonces mostré cómo podríamos habilitar la protección de la falsificación de solicitud de Cross-Site usando el AntiforgeryRequestToken en ASP.NET Core con JavaScript usando HTMX para implementar una caché Donut Hole. Una cuestión pendiente era cómo cargaba las páginas.

El problema

El problema era que yo estaba usando HTMX AJAX para hacer la carga de página solicitada una vez que había seleccionado el resultado de la página desplegable. Esto sólo funcionó KINDA.

  selectResult(result) {
            htmx.ajax('get', result.url, {
                target: '#contentcontainer',  // The container to update
                swap: 'innerHTML',            // Replace the content inside the target
            }).then(function() {
                history.pushState(null, '', result.url);
                window.scrollTo({
                    top: 0,
                    behavior: 'smooth'
                });
            });

El problema era que si bien esto cargaría la página correcta y actualizaría la URL mostrada con la nueva, estropeó el botón de atrás. Como la página NO ERA realmente cargada en la historia correctamente.

Como con mi último artículo sobre shennanigans de botón trasero Esto era algo que quería arreglar.

La solución

Como antes, la solución era dejar que HTMX manejara esto directamente. Para ello actualicé mi plantilla que utilizo para los resultados de búsqueda.

_typeahead.cshtml

<div x-data="window.mostlylucid.typeahead()" class="relative" id="searchelement" x-on:click.outside="results = []">
    @Html.AntiForgeryToken()
    <label class="input input-sm bg-neutral-500 bg-opacity-10 input-bordered flex items-center gap-2">
        <input
            type="text"
            x-model="query"
            x-on:input.debounce.200ms="search"
            x-on:keydown.down.prevent="moveDown"
            x-on:keydown.up.prevent="moveUp"
            x-on:keydown.enter.prevent="selectHighlighted"
            placeholder="Search..."
            class="border-0 grow input-sm text-black dark:text-white bg-transparent w-full"/>
        <i class="bx bx-search"></i>
    </label>
    <!-- Dropdown -->
    <ul x-show="results.length > 0"
        id="searchresults"
        class="absolute z-100 my-2 w-full bg-white dark:bg-custom-dark-bg border border-1 text-black dark:text-white border-neutral-600 rounded-lg shadow-lg">
        <template x-for="(result, index) in results" :key="result.slug">
            <li :class="{
                'dark:bg-blue-dark bg-blue-light': index === highlightedIndex,
                'dark:hover:bg-blue-dark hover:bg-blue-light': true
            }"
                class="cursor-pointer text-sm p-2 m-2">
                <!-- These are the key changes.-->
                <a
                    x-on:click="selectResult(index)"
                    @* :href="result.url" *@
                    :hx-get="result.url"
                    hx-target="#contentcontainer"
                    hx-swap="innerHTML"
                    hx-push-url="true"
                    x-text="result.title"
                   >
                </a>
                <-- End of changes -->
            </li>
        </template>
    </ul>

</div>

Ya verás que ahora genero apropiado Enlaces HTMX en este bloque de código. Permitiéndonos usar el comportamiento correcto de HTMX.

typeahead.js

Para habilitar esto en mi código JavaScript de motor he añadido lo siguiente a mi método de búsqueda (que se muestra a continuación). Los this.$nextTick es una construcción Alpine.js que retrasa esto hasta que Alpine haya terminado de procesar la plantilla que mostré arriba.

A continuación, uso htmx.process() en el elemento de búsqueda que garantizará que los atributos HTMX funcionen como se esperaba.


.then(data => {
 this.results = data;
this.highlightedIndex = -1; // Reset index on new search
 this.$nextTick(() => {
    htmx.process(document.getElementById('searchresults'));
 });
})
typeahead.js search
   search() {
            if (this.query.length < 2) {
                this.results = [];
                this.highlightedIndex = -1;
                return;
            }
            let token = document.querySelector('#searchelement input[name="__RequestVerificationToken"]').value;
            console.log(token);
            fetch(`/api/search/${encodeURIComponent(this.query)}`, { // Fixed the backtick and closing bracket
                method: 'GET', // or 'POST' depending on your needs
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': token // Attach the AntiForgery token in the headers
                }
            })
                .then(response => {
                    if(response.ok){
                        return  response.json();
                    }
                    return Promise.reject(response);
                })
                .then(data => {
                    this.results = data;
                    this.highlightedIndex = -1; // Reset index on new search
                    this.$nextTick(() => {
                        htmx.process(document.getElementById('searchresults'));
                    });
                })
                .catch((response) => {
                    console.log(response.status, response.statusText);
                    if(response.status === 400)
                    {
                        console.log('Bad request, reloading page to try to fix it.');
                        window.location.reload();
                    }
                    response.json().then((json) => {
                        console.log(json);
                    })
                    console.log("Error fetching search results");
                });
        }
Más tarde, una vez que se selecciona una página, manejo el código para seleccionar la página, haga clic en el enlace un claro de los resultados (para cerrar el cuadro de búsqueda).
selectHighlighted() {
            if (this.highlightedIndex >= 0 && this.highlightedIndex < this.results.length) {
                this.selectResult(this.highlightedIndex);
                
            }
        },

        selectResult(selectedIndex) {
       let links = document.querySelectorAll('#searchresults a');
       links[selectedIndex].click();
            this.$nextTick(() => {
                this.results = []; // Clear the results
                this.highlightedIndex = -1; // Reset the highlighted index
                this.query = ''; // Clear the query
            });
        }

Esto se selecciona a través del click del enlace en los resultados de búsqueda.

 <a
  x-on:click="selectResult(index)"
  :hx-get="result.url"
  hx-target="#contentcontainer"
  hx-swap="innerHTML"
  hx-push-url="true"
  x-text="result.title"
  >
  </a>

Que luego cargará la página y actualizará la URL correctamente.

También tengo código en la caja madre whick le permite utilizar las teclas de flecha y entrar.

    <label class="input input-sm bg-neutral-500 bg-opacity-10 input-bordered flex items-center gap-2">
        <input
            type="text"
            x-model="query"
            x-on:input.debounce.200ms="search"
            x-on:keydown.down.prevent="moveDown"
            x-on:keydown.up.prevent="moveUp"
            x-on:keydown.enter.prevent="selectHighlighted"
            placeholder="Search..."
            class="border-0 grow input-sm text-black dark:text-white bg-transparent w-full"/>
        <i class="bx bx-search"></i>
    </label>

Verá que esto tiene todo el código necesario para permitirle simplemente pulsar enter y navegar a la página seleccionada.

Conclusión

Sólo un artículo de actualización rápida al menú desplegable de búsqueda existente para mejorar la experiencia del usuario cuando se utiliza la búsqueda. Una vez más, este es un usuario MINIMAL frente al cambio, pero sólo mejora la experiencia del usuario; que como desarrollador web son su principal preocupación (más allá de recibir el pago :)).

logo

©2024 Scott Galloway