Computer Science
Algorithm
Data Processing
Digital Life
Distributed System
Distributed System Infrastructure
Machine Learning
Operating System
Android
Linux
MacOS
Tizen
Windows
iOS
Programming Language
C++
Erlang
Go
Scala
Scheme
Type System
Software Engineering
Storage
UI
Flutter
Javascript
Create a Checkbox That Returns Boolean Value for htmx (2024)
Virtualization
Life
Life in Guangzhou (2013)
Recent Works (2013)
东京之旅 (2014)
My 2017 Year in Review (2018)
My 2020 in Review (2021)
十三年前被隔离的经历 (2022)
A Travel to Montreal (2022)
My 2022 in Review (2023)
Travel Back to China (2024)
Projects
Bard
Blog
RSS Brain
Scala2grpc
Comment Everywhere (2013)
Fetch Popular Erlang Modules by Coffee Script (2013)
Psychology
耶鲁大学心理学导论 (2012)
Thoughts
Chinese
English

Create a Checkbox That Returns Boolean Value for htmx

Posted on 08 Jun 2024, tagged HTMLhtmxJavascriptfrontendtech

The Problem of Checkbox

htmx is a lightweight Javascript framework. We all know in native HTML, a form element can send a HTTP request to a server with the values of input elements. In htmx, this feature is made more powerful and flexible: you can include the value of any element, and with the help with htmx extensions like json-enc, it can also post JSON data.

However, there is one thing that htmx inherited from the native HTML form behaviour: for checkboxes, it only includes its value when the checkbox is checked. And the default value for checkbox is "on" instead of true (even though you can change it to another value). I understand this decision because it wants to keep the same behaviour so there is no surprise, but it also makes the backend parsing very inconvenient. The checkbox field needs some special treatment at the backend: you need to know there is a checkbox field so that you can set it to false when it’s not submitted with the request, and set it to true otherwise.

In this article, we will explore how to define a custom checkbox element so that it has a boolean value and will always be submitted with the HTTP request. We first explore the implementation for htmx and then for native HTML.

How htmx Submit the Checkbox Value

In order to make it work with htmx, we first need to know how htmx do the HTTP request with parameters. The document doesn’t have a lot of details but we can always check the source code. The code that processes input values is in the function processInputValue:

function processInputValue(processed, values, errors, elt, validate) {
    if (elt == null || haveSeenNode(processed, elt)) {
        return;
    } else {
        processed.push(elt);
    }
    if (shouldInclude(elt)) {
        var name = getRawAttribute(elt,"name");
        var value = elt.value;
        if (elt.multiple && elt.tagName === "SELECT") {
            value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
        }
        // include file inputs
        if (elt.files) {
            value = toArray(elt.files);
        }
        addValueToValues(name, value, values);
        if (validate) {
            validateElement(elt, errors);
        }
    }
    if (matches(elt, 'form')) {
        var inputs = elt.elements;
        forEach(inputs, function(input) {
            processInputValue(processed, values, errors, input, validate);
        });
    }
}

So it checks whether the element should be included through function shouldInclude(elt) and get its value if so (some additional logic for select and file but it’s not a concern here). In shouldInclude, it will only include a checkbox if it’s checked:

function shouldInclude(elt) {
    if(elt.name === "" || elt.name == null || elt.disabled || closest(elt, "fieldset[disabled]")) {
        return false;
    }
    // ignore "submitter" types (see jQuery src/serialize.js)
    if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
        return false;
    }
    if (elt.type === "checkbox" || elt.type === "radio" ) {
        return elt.checked;
    }
    return true;
}

Create a Custom Checkbox Element with Web Component

I tried to find or write an extension for htmx to include checkbox elements with boolean values, but from what I learnt in the htmx extension doc, there is no good way to do that. So I decided to create a custom HTML element that extends input to return boolean values for htmx to get.

With web component, we can create a HTML tag that can be used just like any other built-in HTML tags. The MDN guide does a good job to explain how to do it so I will not repeat it here. I’ll just put my implementation of the customized checkbox here:

class BooleanCheckbox extends HTMLInputElement {
    constructor() {
        super();
    }

    get checked() {
        return true;
    }

    get value() {
        if (super.checked) {
            return true;
        } else {
            return false;
        }
    }
}

customElements.define("boolean-checkbox", BooleanCheckbox, { extends: "input" });

You can see it’s very simple. It extends the input element. It overwrite checked to always return true so that htmx will always include it in the request. And for value, it returns a boolean depends on super.checked. At last it register the customized element as a tag namedjboolean-checkbox, so that we can just use it like this in HTML:

<input type="checkbox" is="boolean-checkbox" />Boolean checkbox

The is="boolean-checkbox" part tells the browser that this is a customized input element.

Here is a complete example:

<!DOCTYPE html>
<html>
  <head>
    <title>htmx boolean checkbox example</title>
    <script src="https://unpkg.com/htmx.org@1.9.12"></script>
    <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js"></script>
    <script>
      class BooleanCheckbox extends HTMLInputElement {
          constructor() {
              super();
          }

          get checked() {
              return true;
          }

          get value() {
              if (super.checked) {
                  return true;
              } else {
                  return false;
              }
          }
      }
      customElements.define("boolean-checkbox", BooleanCheckbox, { extends: "input" });
    </script>
  </head>

  <body>
    <form>
      <div><input type="checkbox" name="default-checkbox" />Default checkbox</div>
      <div><input type="checkbox" is="boolean-checkbox" name="boolean-checkbox" />Boolean checkbox</div>
      <button hx-post="test-post" hx-ext="json-enc">Submit</button>
    </form>
  </body>
</html>

It defines two checkboxes: a native one and a customized one. We use the json-enc extension so it will post JSON as request body. When click the submit button, if both of them are unchecked, the post body looks like this:

{"boolean-checkbox":false}

And if both are selected, here is the post body:

{"default-checkbox":"on","boolean-checkbox":true}

What About the Native HTML Form Action

The custom element boolean-checkbox only works with htmx to post boolean values. If you use native form action like this:

<form action="test-call">
  <input is="boolean-checkbox" type="checkbox" name="boolean-checkbox">Boolean Checkbox</input>
  <button>Submit</button>
</form>

The behaviour is still like the native checkbox, which only posts value “on” when it’s checked.

Even though I don’t use the native form action, it still makes me wonder if I can support it. (Disclaimer: all the code below are experiments and I don’t recommend anyone uses it on production without careful tests.)

In fact, there is a way to set form value in web component through ElementInternals.setFormValue:

this.internals = this.attachInternals();
this.internals.setFormValue(this.value);

However, in HTML standard, ElementInternals is not supported if the custom element is extending a built-in input element. Actually there is a Github issue asking for this feature, and the response to not support it doesn’t make sense to me:

Since Apple’s WebKit team’s position is that customized builtins shouldn’t exist in the first place, we don’t support this proposal.

Anyway, it is what it is. So I need to workaround it. The solution I came up is to include another checkbox element as a child instead of inherit it. Here is the code:

class BooleanCheckbox extends HTMLElement {

    static formAssociated = true;

    constructor() {
        super();
        this.internals = this.attachInternals();
    }

    connectedCallback() {
        this.shadow = this.attachShadow({mode: "open"});
        const internalCheckbox = document.createElement("input");
        internalCheckbox.setAttribute("type", "checkbox");
        this.getAttributeNames().forEach((name) => {
            internalCheckbox.setAttribute(name, this.getAttribute(name));
        });
        this.shadow.appendChild(internalCheckbox);
        this.internals.setFormValue(internalCheckbox.value);
        internalCheckbox.addEventListener('change', () => {
            this.internals.setFormValue(this.value);
        });
    }

    get checkbox() {
        return this.shadow.querySelector("input[type=checkbox]");
    }


    get checked() {
        return true;
    }

    get value() {
        if (this.checkbox.checked) {
            return true;
        } else {
            return false;
        }
    }

}

customElements.define("boolean-checkbox", BooleanCheckbox);

It listens on the checked attribute on the child checkbox and update the form value based on it. static formAssociated = true; is needed so that we can set form values.

Then in HTML, we can use it like this:

<form action="/test-call">
  <div><input type="checkbox" name="default-checkbox" />Default Checkbox</div>
  <div><boolean-checkbox name="boolean-checkbox"></boolean-checkbox>Boolean Checkbox</div>
  <div><button>Submit</button></div>
</form>

When click the submit button, it calls /test-call?boolean-checkbox=false if both checkboxes are unchecked and /test-call?default-checkbox=on&boolean-checkbox=true if both are checked.