A Whistle-stop Tour di estensioni HTMX e l'utilizzo di HTMX con ASP.NET Core (Italiano (Italian))

A Whistle-stop Tour di estensioni HTMX e l'utilizzo di HTMX con ASP.NET Core

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.

Friday, 02 May 2025

//

14 minute read

Introduzione

HTMX è una potente libreria JavaScript che consente di creare applicazioni web dinamiche con un minimo di JavaScript. Consente di effettuare richieste AJAX, scambiare contenuti HTML e gestire gli eventi direttamente negli attributi HTML. Sto usando HTMX per circa due anni a questo punto e con ogni progetto imparo sempre di più sulle sue capacità; e soprattutto sono limitazioni.

Tuttavia ancora non pretendo di avere una conoscenza esperta di esso. Volevo solo condividere alcune delle cose che ho imparato lungo il percorso.

Manifestazioni

Richiesta di preparazione

La fase di preparazione della richiesta è quella in cui HTMX configura la richiesta prima di essere inviata al server. Questo include l'impostazione delle intestazioni, l'aggiunta di parametri e la gestione degli input utente. Durante questa fase si attivano i seguenti eventi:

flowchart LR A[htmx:configRequest] --> B[htmx:confirm] --> C[htmx:prompt] --> D[htmx:abort]

Richiedi ciclo di vita

La fase del ciclo di vita della richiesta è quella in cui HTMX invia la richiesta al server e gestisce la risposta. Durante questa fase si attivano i seguenti eventi:

flowchart LR E[htmx:beforeRequest] --> F[htmx:request] --> G[htmx:afterRequest]

Gestione della risposta

La fase di gestione della risposta è quella in cui HTMX elabora la risposta dal server e aggiorna il DOM. Durante questa fase si attivano i seguenti eventi:

flowchart LR H[htmx:beforeOnLoad] --> I[htmx:onLoad] I --> J[htmx:beforeSwap] --> K[htmx:swap] --> L[htmx:afterSwap] L --> M[htmx:afterSettle] --> N[htmx:afterOnLoad]

Gestione cronologia

La fase di gestione della cronologia è dove HTMX aggiorna la cronologia del browser e l'URL. Durante questa fase si attivano i seguenti eventi:

flowchart LR A[htmx:historyRestoreRequest] A --> B[htmx:historyPopped] B --> C[htmx:historyRestorePage]

Come si può vedere HTMX fornisce una serie di eventi che è possibile collegare per modificare la richiesta o la risposta, o anche la cronologia; HTMX fa un LOT per essere un sistema così compatto. Utilizzando ciascuno di questi è possibile modificare il modo in cui HTMX interagisce con il server / il client in modo abbastanza completo.

Estensioni

Uno degli aspetti più potenti di HTMX è la capacità di crea estensioni estendere le sue capacità. Nel mio caso, di solito mi aggrappai htmx:configRequest aggiungere parametri aggiuntivi alla richiesta. Questo è utile quando si desidera passare dati aggiuntivi al server senza dover modificare il codice HTML o JavaScript.

Altre estensioni potrebbero agganciare htmx:beforeRequest modificare la richiesta prima dell'invio; ma dopo la maggior parte delle altre estensioni che agganciano configRequest; come in beforeRequest roba come... HX-Vals e HX-Includes sono già attaccati al réuest (sia nel payload \ querystring). Puoi anche agganciare htmx:afterSwap per eseguire azioni dopo che il contenuto è stato scambiato. Combinato con librerie di templating lato client come Alpine.js oppure LitCity name (optional, probably does not need a translation) è possibile creare potenti applicazioni dinamiche con codice minimo.

HTMX fornisce alcune estensioni integrate come hx-boost e hx-swap-oob che consentono di migliorare la funzionalità di HTMX senza scrivere alcun codice personalizzato. Tuttavia, ci sono momenti in cui è necessario creare le proprie estensioni per soddisfare requisiti specifici.

Ad esempio, potresti voler aggiungere intestazioni personalizzate alle tue richieste, modificare il payload della richiesta o gestire eventi specifici in modo unico.

Per ottenere questo HTMX offre alcuni comodi punti di integrazione:


{
  /**
   * init(api)
   * Called once when the extension is initialized.
   * Use it to set up internal state, store references, or access HTMX utility functions via the api parameter.
   */
  init: function(api) {
    return null;
  },

  /**
   * getSelectors()
   * Returns additional CSS selectors that HTMX should monitor.
   * Useful if your extension needs to handle custom elements or dynamic behavior.
   */
  getSelectors: function() {
    return null;
  },

  /**
   * onEvent(name, evt)
   * Called on every HTMX event (e.g., htmx:beforeRequest, htmx:afterSwap).
   * Return false to cancel the event or stop propagation.
   */
  onEvent: function(name, evt) {
    return true;
  },

  /**
   * transformResponse(text, xhr, elt)
   * Modify the raw response text before it is parsed and swapped into the DOM.
   * Use this to sanitize or preprocess HTML.
   */
  transformResponse: function(text, xhr, elt) {
    return text;
  },

  /**
   * isInlineSwap(swapStyle)
   * Return true if your extension will handle this swap style manually.
   * This tells HTMX to skip default behavior.
   */
  isInlineSwap: function(swapStyle) {
    return false;
  },

  /**
   * handleSwap(swapStyle, target, fragment, settleInfo)
   * Perform custom DOM manipulation if you implement a custom swap style.
   * Return true to prevent HTMX's default swap.
   */
  handleSwap: function(swapStyle, target, fragment, settleInfo) {
    return false;
  },

  /**
   * encodeParameters(xhr, parameters, elt)
   * Modify or serialize request parameters before sending.
   * Return null to use default URL/form encoding.
   * Return a string to override with a custom payload (e.g., JSON).
   */
  encodeParameters: function(xhr, parameters, elt) {
    return null;
  }
}

HTMX fornisce una serie di estensioni precostruite è possibile leggi qui.

Per esempio un pratico estensione incorporata json-encode consente di inviare dati JSON nell'organismo di richiesta invece di dati URL-encoded form. Questo è utile quando si desidera inviare strutture dati complesse o array al server. Potete vedere che questo aggancia in 3 eventi

  • init - impostare l'estensione e memorizzare un riferimento all'API HTMX
  • onEvent - di impostare il Content-Type intestazione a application/json quando la richiesta è configurata
  • encodeParameters - per sovrascrivere la codifica predefinita URL-encoded form e serializzare i parametri come JSON. Restituisce anche una stringa per impedire ad HTMX di utilizzare la codifica predefinita URL-encoded form.
(function() {
  let api
  htmx.defineExtension('json-enc', {
    init: function(apiRef) {
      api = apiRef
    },

    onEvent: function(name, evt) {
      if (name === 'htmx:configRequest') {
        evt.detail.headers['Content-Type'] = 'application/json'
      }
    },

    encodeParameters: function(xhr, parameters, elt) {
      xhr.overrideMimeType('text/json')

      const object = {}
      parameters.forEach(function(value, key) {
        if (Object.hasOwn(object, key)) {
          if (!Array.isArray(object[key])) {
            object[key] = [object[key]]
          }
          object[key].push(value)
        } else {
          object[key] = value
        }
      })

      const vals = api.getExpressionVars(elt)
      Object.keys(object).forEach(function(key) {
        // FormData encodes values as strings, restore hx-vals/hx-vars with their initial types
        object[key] = Object.hasOwn(vals, key) ? vals[key] : object[key]
      })

      return (JSON.stringify(object))
    }
  })
})()

O anche il più semplice ma anche più utile hx-debug estensione che aggiunge a HX-Debug Intestazione della richiesta. Questo è utile per scopi di debug e registrazione, in quanto consente di visualizzare i dati di richiesta e risposta grezzi nella console dev.

(function() {
  htmx.defineExtension('debug', {
    onEvent: function(name, evt) {
      if (console.debug) {
        console.debug(name, evt)
      } else if (console) {
        console.log('DEBUG:', name, evt)
      } else {
        throw new Error('NO CONSOLE SUPPORTED')
      }
    }
  })
})()

Ce ne sono molti di più; compreso un molto potente estensione templatura lato client che consente di utilizzare le librerie di templating lato client per trasformare i dati JSON restituiti in HTML. Questo è utile per creare UI dinamici senza dover fare affidamento sul rendering lato server.

Alcune estensioni personalizzate

ID della riga dinamica

Ad esempio in un recente progetto ho usato HTMX OOB swaps per aggiornare un certo numero di righe in una tabella. Per fare questo volevo sapere quali righe erano attualmente visualizzate nella tabella, quindi ho aggiornato solo le righe che erano visibili.

L'estensione

export default {
    encodeParameters: function (xhr, parameters, elt) {
        const ext = elt.getAttribute('hx-ext') || '';
        if (!ext.split(',').map(e => e.trim()).includes('dynamic-rowids')) {
            return null; // Use default behavior
        }

        const id = elt.dataset.id;
        const approve = elt.dataset.approve === 'true';
        const minimal = elt.dataset.minimal === 'true';
        const single = elt.dataset.single === 'true';

        const target = elt.dataset.target;
        const payload = { id, approve, minimal, single };

        if (approve && target) {
            const table = document.querySelector(target);
            if (table) {
                const rowIds = Array.from(table.querySelectorAll('tr[id^="row-"]'))
                    .map(row => row.id.replace('row-', ''));
                payload.rowIds = rowIds;
            }
        }

        // Merge payload into the parameters object
        Object.assign(parameters, payload);
        return null; // Return null to continue with default URL-encoded form encoding
    }
}

Usarlo

Per utilizzarlo dobbiamo aggiungere l'estensione alla nostra configurazione HTMX. Quindi nel tuo file js del punto di ingresso (supponendo che stai usando moduli; yoy dovrebbe essere) puoi fare una cosa del genere:

import dynamicRowIds from "./dynamicRowIds"; // Load the file

htmx.defineExtension("dynamic-rowids", dynamicRowIds); // Register the extension

Poi su qualsiasi elemento si desidera utilizzare su si può aggiungere il hx-ext attributo con il valore dynamic-rowids.

                <button
                    hx-ext="dynamic-rowids"
                    data-target="#my-table"
                    data-id="@Model.Id"
                    data-param1="true"
                    data-param2="false"
                    data-param3="@Model.Whatever"
                    hx-post
                    hx-controller="Contoller"
                    hx-action="Action"
                >
                    <i class='bx bx-check text-xl text-white'></i>
                </button>

Preserva parami

Questa è un'altra semplice estensione HTMX, questa volta agganciata htmx:configRequest mentre stiamo modificando l'URL prima dell'invio della richiesta. Questa estensione è utile se si utilizza il filtro basato su querystring etc. e vuoi che alcune richieste conservino i filtri esistenti mentre altre no (ad esempio 'name' e'startdate' ma non 'page' o'sort').

Questo è SIMILE a ma non esattamente lo stesso come l'estensione esistente HTMX push-params

L'estensione

Puoi vedere che ci agganciamo onEvent per ascoltare per il htmx:configRequest evento.

Allora noi:

  • Ottieni l'elemento che ha attivato l'evento
  • Prendi il... preserve-params-exclude attributo dall'elemento (se esiste) e dividerlo in un array di tasti da escludere (così non li aggiungiamo alla richiesta)
  • Ottieni i parametri URL attuali dalla posizione della finestra
  • Ottieni i nuovi parametri dall'URL della richiesta
  • Loop attraverso i parametri attuali e controllare se non sono nella lista di esclusione e non già nei nuovi parametri
  • Se non lo sono, li aggiungiamo ai nuovi parametri
  • Infine, abbiamo impostato i nuovi parametri per l'URL richiesta e tornare true per continuare con la richiesta.
export default {
    onEvent: function (name, evt) {
        if (name !== 'htmx:configRequest') return;
        const el = evt.detail.elt;
        const excludeStr = el.getAttribute('preserve-params-exclude') || '';
        const exclude = excludeStr.split(',').map(s => s.trim());

        const currentParams = new URLSearchParams(window.location.search);
        const newParams = new URLSearchParams(evt.detail.path.split('?')[1] || '');

        currentParams.forEach((value, key) => {
            if (!exclude.includes(key) && !newParams.has(key)) {
                newParams.set(key, value);
            }
        });

        evt.detail.path = evt.detail.path.split('?')[0] + '?' + newParams.toString();

        return true;
    }
};

Qui uso l'essenziale HTMX.Net per i suoi aiutanti di tag. I tipi di... hx-controller e hx-action sono aiutanti di tag che generano i corretti attributi HTMX per voi. Cosi' come... hx-route-<x> per i valori da passare nel percorso. Questo è davvero utile in quanto consente di utilizzare il codice C# per generare i valori corretti per gli attributi invece di doverli codificare in modo rigido nel tuo HTML.

Usarlo

Essendo un'estensione è molto semplice da usare:

Per prima cosa dobbiamo aggiungere l'estensione alla nostra configurazione HTMX.


import preserveParams from './preserveParams.js';
htmx.defineExtension('preserve-params', preserveParams);

NOTA: noterete che le estensioni HTMX predefinite usano il metodo 'autoload' per caricare l'estensione.

// Autoloading the extension and registering it
(function() {
  htmx.defineExtension('debug', {
}

Questo è un buon modo per farlo se si utilizza HTMX in un ambiente non-module. Tuttavia, se si utilizzano moduli (che si dovrebbe essere) è meglio utilizzare il import dichiarazione per caricare l'estensione quindi registrarlo esplicitamente contro il vostro htmx esempio. Questo ti permette di approfittare di tree-shaking e caricare solo le estensioni di cui hai bisogno.

Poi sul tuo elemento puoi aggiungere il hx-ext attributo con il valore preserve-params e della preserve-params-exclude attributo con un elenco separato da virgola di parametri da escludere dalla richiesta.


<a class="btn-outline-icon"
   hx-controller="MyController"
   hx-action="MyAction"
   hx-route-myparam="@MyParam"
   hx-push-url="true"
   hx-ext="preserve-params"
   preserve-params-exclude="page,sort"
   hx-target="#page-content"
   hx-swap="innerHTML show:top"
   hx-indicator>
    <i class="bx bx-search"></i>
</a>

In questo caso a causa della event.detail.path avere il nuovo myparam valore impostato in esso sostituirà quello con il nostro nuovo valore ma conservare tutti gli altri (tranne page e sort). Quindi possiamo continuare a passare tutti i filtri che abbiamo impostato nell'URL al server senza doverli preoccupare di perderli quando facciamo una nuova richiesta.

ASP.NET Core

Una delle cose belle di HTMX è che gran parte della sua interazione con il server avviene attraverso le intestazioni HTTP. Queste intestazioni forniscono al server un contesto ricco su ciò che ha innescato la richiesta, permettendoti di rispondere in modo appropriato dagli endpoint ASP.NET Core o dalle viste Razor.

Ancora una volta un componente chiave di questo è il HTMX.Net. Tra molti degli elementi che fornisce sono ordinati Request estensioni per rilevare le richieste di HTMX. Ciò è utile per determinare se una richiesta è stata fatta da HTMX o no, e per gestirla di conseguenza.

Ha anche un proprio meccanismo per inviare i trigger


Response.Htmx(h => {
    h.WithTrigger("yes")
     .WithTrigger("cool", timing: HtmxTriggerTiming.AfterSettle)
     .WithTrigger("neat", new { valueForFrontEnd= 42, status= "Done!" }, timing: HtmxTriggerTiming.AfterSwap);
});

Push Urls etc...etc... Khalid ha fatto un ottimo lavoro di creazione di un set di estensioni per rendere facile lavorare con HTMX in ASP.NET Core.

È uno strumento chiave nella mia cassetta degli strumenti quando si lavora con HTMX e ASP.NET Core. Guarda qui!

Intestazioni comuni di richiesta HTMX

Qui si trova una ripartizione delle intestazioni più utili HTMX invia ad ogni richiesta:

Intestazione Descrizione
HX-Richiesta Sempre impostato su VERO per qualsiasi richiesta avviata da HTMX. Ottimo per rilevare chiamate HTMX in middleware o controller.
HX-Target The id of the target element in the DOM that the response will be swaped in.
HX-Trigger L'id dell'elemento che ha attivato la richiesta (es. un pulsante).
HX-Trigger-Name Il nome dell'elemento di attivazione (utile per le forme).
HX-Prompt Contiene l'ingresso dell'utente da hx-prompt.
HX-Current-URL L'URL del browser quando la richiesta è stata avviata. Utile per la registrazione e il contesto.
HX-History-Restore-Richiesta Invia come true se la richiesta fa parte di un ripristino della cronologia dopo la navigazione (ad esempio, pulsante posteriore).

Richiesta estensioni

Li uso abbastanza ampiamente nelle mie applicazioni ASP.NET Core. In combinazione con i modelli HDMX.Net come Request.IsHtmx() , Request.IsHtmxBoosted() e Request.IsHtmxNonBoosted() è possibile rilevare facilmente le richieste di HTMX e rispondere di conseguenza.

Per esempio, ho un'estensione davvero semplice alla richiesta che mi permette di rilevare se una richiesta sta prendendo di mira la mia principale #page-content Div. Se è allora so che dovrei rispedire indietro un parziale. NOTA: Molti non si rendono conto che è possibile specificare una 'pagina completa' come parziale, quindi salta solo il layout.

        if (Request.PageContentTarget())
        {   
            Response.PushUrl(Request);
            return PartialView("List", vm);
        }
        return View("List", vm);
        
        public static class RequestExtensions
{
        
        public static bool PageContentTarget(this HttpRequest request)
    {
        bool isPageContentTarget = request.Headers.TryGetValue("hx-target", out var pageContentHeader) 
                                   && pageContentHeader == "page-content";
        
        return isPageContentTarget;
    }
    }

Estensioni di risposta

Oltre a Richiedi estensioni, puoi anche creare estensioni Response per inviare eventi al client. Questo è utile per innescare eventi lato client.

Esempio SweetAlert

Ad esempio nella mia integrazione SweetAlert2 Abilito la chiusura della finestra di dialogo usando un trigger impostato dal server.

    document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);

Questo viene attivato dal server come evento di attivazione HTMX.


    public static void CloseSweetAlert(this HttpResponse response)
    {
        response.Headers.Append("HX-Trigger" , JsonSerializer.Serialize(new
        {
            sweetalert = "close"
        }));

    }

Questo attiverà il sweetalert:close evento sul lato client, che consente di chiudere la finestra di dialogo. È inoltre possibile trasferire i dati al client utilizzando il HX-Trigger Intestazione. Questo è utile per trasferire i dati dal server al client senza dover modificare il codice HTML o JavaScript.

Come vedete è facile ascoltare questi eventi semplicemente aggiungendo un ascoltatore di eventi al corpo. Uso JSON principalmente perché codifica sempre correttamente.

Mostratorno

Ho scritto del mio metodo di brindisi in precedenza quiMa vale la pena menzionare anche qui. Per consentire al server di attivare una notifica di brindisi sul lato client. Ho impostato il grilletto in questa estensione di risposta.

    public static void ShowToast(this HttpResponse response, string message, bool success = true)
    {
        response.Headers.Append("HX-Trigger", JsonSerializer.Serialize(new
        {
            showToast = new
            {
                toast = message,
                issuccess =success
            }
        }));

    }

Poi ho agganciato nel lato client evento e chiamare il mio showToast funzione.

import { showToast, showHTMXToast } from './toast';

window.showHTMXToast = showHTMXToast;

document.body.addEventListener("showToast", showHTMXToast);

Questo poi chiama nel mio showToast funzione e bene, mostra un brindisi; di nuovo vedere di più su di esso nell'articolo .



export function showHTMXToast(event) {
    const xhr = event?.detail?.xhr;
    let type = 'success';
    let message = xhr?.responseText || 'Done!';

    try {
        const data = xhr ? JSON.parse(xhr.responseText) : event.detail;

        if (data.toast) message = data.toast;
        if ('issuccess' in data) {
            type = data.issuccess === false ? 'error' : 'success';
        } else if (xhr?.status >= 400) {
            type = 'error';
        } else if (xhr?.status >= 300) {
            type = 'warning';
        }

    } catch {
        if (xhr?.status >= 400) type = 'error';
        else if (xhr?.status >= 300) type = 'warning';
    }

    showToast(message, 3000, type);
}

In conclusione

Beh, questo è tutto, un giro vorticoso di HTMX e ASP.NET Core. Spero che tu l'abbia trovato utile e informativo. Se avete domande o commenti non esitate a commentare qui sotto.

logo

©2024 Scott Galloway