An Alpine.js and HTMX 'Clear Query String Parameter(s)' tag helper for ASP.NET Core (English)

An Alpine.js and HTMX 'Clear Query String Parameter(s)' tag helper for 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, 25 April 2025

//

Less than a minute

Introduction

Just a quick one, I had a need in a work project for the ability to 'clear' URL parameters from a URL. This is useful when you have a URL with multiple parameters, and you want to remove one or more of them (for example for a search filter).

The Problem

My current project uses old-school query strings (it's an admin site so doesn't need the fanciness of 'nice' urls). So I wind up with a URL like this:

/products?category=electronics&search=wireless+headphones&sort=price_desc&inStock=true&page=3

Now these can vary with each page, so I can end up with a BUNCH in the page URL and I need to be able to clear them out without writing a bunch of boilerplate to do it.

You CAN do this as part of whatever input control you use so for instance next to each checkbox (or a fancy placeholder style clear icon) but and you can use this technique for those too. However, in this case I wanted to do two main things:

  1. Be able to clear a named parameter
  2. Be able to clear a list of parameters.
  3. Be able to clear all parameters
  4. Have it post back with HTMX
  5. Have it use my loading indicator my loading indicator.

The Solution

In my project I already use

  • HTMX
  • Alpine.js
  • ASP.NET Core
  • TailwindCSS
  • DaisyUI

So my solution was focused around using these to get a nice looking, functional solution with minimal code.

The Tag Helper

My TagHelper is pretty simple, all I do is create an <a> tag with a few attributes I'll later pass into the Alpine Module and we're done.

[HtmlTargetElement("clear-param")]
public class ClearParamTagHelper : TagHelper
{
    [HtmlAttributeName("name")]
    public string Name { get; set; }
    
    [HtmlAttributeName("all")]
    public bool All { get; set; }= false;
    
    [HtmlAttributeName("target")]
    public string Target { get; set; } = "#page-content";

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "a";
        output.Attributes.SetAttribute("x-data", "window.queryParamClearer({})");

        if (All)
        {
        output.Attributes.SetAttribute("x-all", All);
        }
        else
        {
            output.Attributes.SetAttribute("x-param", Name);
        }
        output.Attributes.SetAttribute("data-target", Target);
        output.Attributes.SetAttribute("x-on:click.prevent", "clearParam($event)");
        output.Content.SetHtmlContent(@"
            <div class='w-6 h-6 flex items-center justify-center bg-red-600 text-white rounded-full'>
                <i class='bx bx-x text-lg'></i>
            </div>");
    }
}

Parameters

In use this looks like this, first for 'clear all parameters'. So I just look at the Context.Request.Query if there's any parameters there I render the little x icon to let the user clear all the parameters.


@if(Context.Request.Query.Any())
{
<label class="param-label">
    <clear-param all="true"></clear-param>
    clear all
</label>
}
</div>

Alternatively for named parameters I can do this


<div class="param-label">
    <clear-param name="myParam"></clear-param>
    <p>My Param: @Model.MyParam</p>
</div>

Which would of course clear that single parameter.

Or even


<div class="param-label">
    <clear-param name="myParam1,myParam2,myParam3"></clear-param>
    <p>My Param: @Model.MyParam1</p>
    <p>My Param: @Model.MyParam2</p>
    <p>My Param: @Model.MyParam3</p>
</div>

This then clears all the named parameters from the string.

The target attribute

YOu also have the option to pass in a target attribute which will be used as the hx-target attribute. This is useful if you want to update a specific part of the page with the new content.


<div class="param-label">
    <clear-param name="myParam" target="#my-thing"></clear-param>
    <p>My Param: @Model.MyParam</p>
</div>

In my case (because I wroted it) I defaulted the target to my #page-content div.

    [HtmlAttributeName("target")]
    public string Target { get; set; } = "#page-content";

The Result

These result in the rendering of the following HTML:

  • All: So we get HTML with the x-all attribute and no x-param attribute.
<a x-data="window.queryParamClearer({})" x-all="True" data-target="#page-content" x-on:click.prevent="clearParam($event)">
    <div class="w-6 h-6 flex items-center justify-center bg-red-600 text-white rounded-full">
        <i class="bx bx-x text-lg"></i>
    </div>
</a>

  • Single We get HTML with the x-param attribute and no x-all attribute.
<a x-data="window.queryParamClearer({})" x-param="myParam" data-target="#page-content" x-on:click.prevent="clearParam($event)">
    <div class="w-6 h-6 flex items-center justify-center bg-red-600 text-white rounded-full">
        <i class="bx bx-x text-lg"></i>
    </div>
</a>
  • Multiple We get HTML with the x-param attribute with a comma separated string and no x-all attribute.
<a x-data="window.queryParamClearer({})" x-param="myParam1,myParam2,myParam3" data-target="#page-content" x-on:click.prevent="clearParam($event)">
    <div class="w-6 h-6 flex items-center justify-center bg-red-600 text-white rounded-full">
        <i class="bx bx-x text-lg"></i>
    </div>
</a>

Each of them also has the two Alpine attributes x-data and x-on:click.prevent which are used to set up the Alpine module and call the function to clear the parameters.

We'll see how that works next...

The Alpine Module

This is of course made possible through the use of Alpine.js to configure our request and HTMX to perform it.

As you can see in the code below, I have a simple module that takes the path of the current page and then uses the URL API to parse the query string (you could also pass in a different for whatever reason :)) .

We then get the element which was clicked and check if it has the x-all attribute; if it does we delete all the parameters from the URL, otherwise we split the x-param attribute by commas and delete each of those parameters.

Then we create a new URL with the updated query string and use HTMX to make a request to that URL.

export function queryParamClearer({ path = window.location.pathname }) {
    return {
        clearParam(e) {
            const el = e.target.closest('[x-param],[x-all]');
            if (!el) return;

            const url = new URL(window.location.href);

            if (el.hasAttribute('x-all')) {
                // → delete every single param
                // we copy the keys first because deleting while iterating modifies the collection
                Array.from(url.searchParams.keys())
                    .forEach(key => url.searchParams.delete(key));
            } else {
                // → delete only the named params
                (el.getAttribute('x-param') || '')
                    .split(',')
                    .map(p => p.trim())
                    .filter(Boolean)
                    .forEach(key => url.searchParams.delete(key));
            }

            const qs = url.searchParams.toString();
            const newUrl = path + (qs ? `?${qs}` : '');

            showAlert(newUrl);
            htmx.ajax('GET', newUrl, {
                target: el.dataset.target || el.getAttribute('hx-target') || 'body',
                swap: 'innerHTML',
                pushUrl: true
            });
        }
    };
}

//In your entry point / anywhere you want to register the module
import { queryParamClearer } from './param-clearer.js'; // webpackInclude: true

window.queryParamClearer = queryParamClearer;

The showAlert function using SweetAlert2

You'll also note that I call a showAlert function. This is just a simple wrapper around the SweetAlert2 loading indicator I use in my project. You can of course replace this with whatever you want to do.'

This is slightly tweaked from the last time we saw it. So I could extract the showAlert function and make it available to external modules. Which let's me use it in both the param-clearer module and the hx-indicator module.

export function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;

        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;

        // ✅ If this is a pageSize-triggered request, use our custom path
        let path;
        if (evt.detail.headers?.['HX-Trigger-Name'] === 'pageSize') {
            path = getPathWithPageSize(evt.detail);
            console.debug('[SweetAlert] Using custom path with updated pageSize:', path);
        } else {
            path = getRequestPath(evt.detail);
        }

        if (!path) return;
        evt.detail.indicator = null;
        showAlert(path);
    });
}

export function showAlert(path)
{
    const currentPath = sessionStorage.getItem(SWEETALERT_PATH_KEY);

    // Show SweetAlert only if the current request path differs from the previous one
    if (currentPath !== path) {
        closeSweetAlertLoader();
        sessionStorage.setItem(SWEETALERT_PATH_KEY, path);


        Swal.fire({
            title: 'Loading...',
            allowOutsideClick: false,
            allowEscapeKey: false,
            showConfirmButton: false,
            theme: 'dark',
            didOpen: () => {
                // Cancel immediately if restored from browser history
                if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
                    sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
                    Swal.close();
                    return;
                }

                Swal.showLoading();
                document.dispatchEvent(new CustomEvent('sweetalert:opened'));

                // Set timeout to auto-close if something hangs
                clearTimeout(swalTimeoutHandle);
                swalTimeoutHandle = setTimeout(() => {
                    if (Swal.isVisible()) {
                        console.warn('SweetAlert loading modal closed after timeout.');
                        closeSweetAlertLoader();
                    }
                }, SWEETALERT_TIMEOUT_MS);
            },
            didClose: () => {
                document.dispatchEvent(new CustomEvent('sweetalert:closed'));
                sessionStorage.removeItem(SWEETALERT_PATH_KEY);
                clearTimeout(swalTimeoutHandle);
                swalTimeoutHandle = null;
            }
        });
    }
}

//Register it
import { registerSweetAlertHxIndicator, showAlert } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
window.showAlert = showAlert;

As a reminder this uses the path as the key to know when to hide the alert.

HTMX

Finally, we use htmx.ajax to make the request. This is a simple GET request to the new URL we created with the updated query string.

   htmx.ajax('GET', newUrl, {
                target: el.dataset.target || el.getAttribute('hx-target') || 'body',
                swap: 'innerHTML',
                pushUrl: true
            });

In Conclusion

This is a simple way to clear URL parameters using a tag helper and Alpine.js. It allows you to clear all parameters, or just specific ones, with minimal code.

logo

©2024 Scott Galloway