HTMX: Dynamic Url Parameters

HTMXJavaScript
HTMX is a great little library that allows you to make HTTP requests (among other things) from any node in the DOM. There is a lot to say about HTMX and it’s philosophy. The main idea is that the server should respond with html, rather than json. To read more about the HTMX approach please refer to their docs at https://htmx.org/docs/ and their essays at https://htmx.org/essays/. If you have the time, I recommend reading their book HYPERMEDIA SYSTEMS. It outlines a new way to code websites and apps, using tried and true, but largely forgotten, web dev principles.

Often, we need to send url parameters with our request. A straightforward way to do this with HTMX is to add the query params directly on the url.

<button
  hx-get="/records?active=true"
  hx-target="#table"
  hx-swap="outerHTML">
    Get records
</button>

The code above creates this request url: {baseUrl}/records?active=true. The hx-target attribute specifies the CSS selector of the DOM node that should be swapped with the incoming HTML. The hx-swap attribute defines the kind of swap. By default, HTMX uses "innerHTML" which will replace the contents of the hx-target. In our case, we want to replace the entire table, so "outerHTML" is specified.

Another way to write this code is to use the hx-vals attribute.

<button
  hx-get="/records"
  hx-target="#table"
  hx-swap="outerHTML"
  hx-vals="{
    active: true
  }"
>
  Get records
</button>

The code above produces the same url: {baseUrl}/records?active=true.

But often a GET request with params is going to be more complex than this. What about a table that displays records, and the table has a sort button above each column? When you click the sort button it sorts that column asc first. If you click it again it sorts desc. And if you click it a third time, it removes the sort direction.

With behavior like this, you need to keep track of the sort column and the sort direction. The column name can be hard coded in the hx-get or in hx-vals, but the sort direction is dynamic. In this case, a little scripting is in order. Let’s keep track of sort direction state in a name-spaced window object.

// add this script in the head of your html file like below
// <script defer src="/path/to/tableData.js"></script>

// * create namespace add data and methods/behavior
if (!window.tableData) {
  window.tableData = {
    _sortDir: "",
    _sortField: "",

    get sortDir() {
      return this._sortDir;
    },
    set sortDir(val) {
      this._sortDir = val;
    },
    set sortField(field) {
      this._sortField = field;
    },
    get sortField() {
      return this._sortField;
    },

    toggleSort(field) {
      if (field !== this.sortField) {
        // if the sort field is not the same as the last time, then
        // reset the sort direction flow ("" => "asc" => "desc")
        this.sortDir = "";
        this.sortField = field;
      }

      switch (this.sortDir) {
        case "asc":
          this.sortDir = "desc";
          return this.sortDir;
        case "desc":
          this.sortDir = "";
          return this.sortDir;
        case "":
          this.sortDir = "asc";
          return this.sortDir;
        default:
          return this.sortDir;
      }
    },
  };
}

Now, if we add js: before our hx-vals object, we can execute javascript in the params object. We are going to use the toggleSort method from tableData to determine the sort direction. Here is what the mark up would look like for the button above the column for firstName.

<button
  hx-get="/records"
  hx-target="#table"
  hx-swap="outerHTML"
  hx-vals="js:{
    sortDir: tableData.toggleSort('firstName')
    sortField: 'firstName',
  }"
>
  Sort First Name
</button>

The code above produces this url:

{baseUrl}/records?sorDir={value}&sortField=firstName

where {value} is the sort direction returned by the toggleSort() method.

To keep with the philosophy of HTMX, we want to keep scripting to a minimum — logic that can be executed on the server, should be executed on the server. But this is a case where a small piece of state and its corresponding behavioral logic makes sense.

But…

You could just as easily accomplish this entirely on the backend. Keeping with the firstName column, the server could prepare the url to already have the asc direction on load. When a user clicks that, the table head and table contents are replaced. This new code will return desc as the firstName sort direction.

In other words, when the request url is

{baseUrl}/records?sorDir=asc&sortField=firstName

the HTML that comes as a response will replace the firstName sort button url with

{baseUrl}/records?sorDir=desc&sortField=firstName

Note desc in the url. If this new url is the request url, then the response will contain

{baseUrl}/records?sorDir=&sortField=firstName

as the url for the firstName sort button. Note there is no sort direction, meaning the response will use the default sorting that the backend provides. The function used by the server will use the same logic (switch statement) that our script uses.

After sorting the firstName column, if a user clicks sort on, let’s say, the lastName column, the HTML from the server response will set this url on the firstName sort:

{baseUrl}/records?sorDir=asc&sortField=firstName

which is the original url for that sort button. In that same response, the HTML will have the following url in the lastName sort button:

{baseUrl}/records?sorDir=desc&sortField=lastName

as desc is the sort that the user calls when clicking the lastName sort button a second time (the first time, i.e. the original lastName sort url, the direction was asc).

The lesson here is that you can use a little javascript to make request parameters dynamic, but there is often a way to accomplish this on the server. The more logic that is moved from the UI to the server, the easier it is to understand your UI code. Moving UI logic to the server also reduces the amount of code that has to be sent down the wire to users, thereby reducing load time and providing a smoother user experience.

Do you need a software agency?

We want to hear from you!