65.9K
CodeProject is changing. Read more.
Home

HTMX custom json extension that handle array, records, recursive property

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2 votes)

Sep 27, 2024

CPOL

2 min read

viewsIcon

1681

HTMX default json extension is missing some of feature. I created json extension that handle bool array from checkbox list, record array like person object array, recursive property.

Introduction & Background

HTMX is cool and intuitive. We are tired to learn JavaScript framework. HTMX remind me of jQuery era. Easy to use, easy to understand. But as is often to new technology HTMX is some missing feature. One of the feature is to send complex json object to server.

I created a json extension named json-higlabo. It can send json like below that may help your requirement.

 

1. Value array that only checked.

2. Object array that has some property.

3. Recursive object like organization tree

So, let's start!

 

How it works?

You can see sample code in GitHub.

https://github.com/higty/higlabo/tree/master/Net8/HigLabo.Web.Htmx.SampleWebSite

Demo page is here.

https://www.higlabo.ai/htmx-sample

 

0. Setup

Create html file like below.

<html>
<head>
</head>
<body>
    ...some contents...

    <script src="/js/htmx.min.js"></script>
    <script src="/js/json-higlabo.js"></script>
</body>
</html>

json-higlabo.ts is only 190 lines of code. It walks dom tree and theck hig-property-type attribute and create object. At last, create json string from created object.

json-higlabo.ts

window["htmx"].defineExtension("json-higlabo", {
    onEvent: function (name, evt) {
        if (name === "htmx:configRequest") {
            evt.detail.headers["Content-Type"] = "application/json";
        }
    },
    encodeParameters: function (xhr, parameters, element: Element) {
        xhr.overrideMimeType("text/json");

        const ee = new Array<Node>();
        let selector = element.getAttribute("hx-include");
        if (selector != null) {
            if (selector.startsWith("closest ")) {
                selector = selector.substring(8);
                ee.push(element.closest(selector));
            }
            else if (selector == "this") {
                ee.push(element);
            }
            else {
                const ss = selector.split(",");
                for (var i = 0; i < ss.length; i++) {
                    document.querySelectorAll(ss[i]).forEach((e) => {
                        ee.push(e);
                    });
                }
            }
        }

        if (element.getAttribute("hig-property-type") == "Array") {
            let pp = [];
            for (var eIndex = 0; eIndex < ee.length; eIndex++) {
                this.processParameter(pp, ee[eIndex]);
            }
            return JSON.stringify(pp);
        }
        else {
            let p = {};
            for (var eIndex = 0; eIndex < ee.length; eIndex++) {
                this.processParameter(p, ee[eIndex]);
            }
            return JSON.stringify(p);
        }
    },
    processParameter: function (parameter, node: Node) {
        node.childNodes.forEach((childNode) => {
            if (childNode.nodeType == Node.ELEMENT_NODE) {
                const childElement = <Element>childNode;
                const name = childElement.getAttribute("name");

                if (parameter instanceof Array) {
                    if (childElement.getAttribute("hig-property-type") == "Object") {
                        let r = {};
                        parameter.push(r);
                        this.processParameter(r, childNode);
                        return;
                    }
                    else if (childElement.getAttribute("hig-property-type") == "Array") {
                        let rr = [];
                        parameter.push(rr);
                        this.processArrayParameter(rr, childNode);
                        return;
                    }
                    else {
                        if (name == null) {
                            if (childElement.tagName == "INPUT") {
                                let inputElement = childElement as HTMLInputElement;
                                if (childElement.getAttribute("type") == "checkbox") {
                                    if (inputElement.checked == true) {
                                        //Push value to array
                                        parameter.push(inputElement.value);
                                    }
                                }
                                else {
                                    parameter.push(inputElement.value);
                                }
                            }
                        }
                        else {
                            if (childElement.tagName.toLowerCase() == "input" || childElement.tagName.toLowerCase() == "textarea") {
                                let inputElement = childElement as HTMLInputElement;
                                //Push object to array
                                if (childElement.getAttribute("type") == "checkbox") {
                                    parameter.push({
                                        name: name,
                                        value: inputElement.value,
                                        checked: inputElement.checked
                                    });
                                }
                                else {
                                    let r = {};
                                    r[name] = inputElement.value;
                                    parameter.push(r);
                                }
                            }
                            else {
                                let r = {};
                                r[name] = childElement.textContent;
                                parameter.push(r);
                            }
                        }
                    }
                }
                else {
                    if (name != null) {
                        if (childElement.getAttribute("hig-property-type") == "Object") {
                            let r = {};
                            parameter[name] = r;
                            this.processParameter(r, childNode);
                            return;
                        }
                        else if (childElement.getAttribute("hig-property-type") == "Array") {
                            let rr = [];
                            parameter[name] = rr;
                            this.processArrayParameter(rr, childNode);
                            return;
                        }
                        else {
                            if (childElement.tagName.toLowerCase() == "input" || childElement.tagName.toLowerCase() == "textarea") {
                                let inputElement = childElement as HTMLInputElement;
                                if (childElement.getAttribute("type") == "checkbox") {
                                    parameter[name] = inputElement.checked;
                                }
                                else {
                                    parameter[name] = inputElement.value;
                                }
                            }
                            else {
                                parameter[name] = childElement.textContent;
                            }
                        }
                    }
                }
                this.processParameter(parameter, childNode);
            }
        });
    },
    processArrayParameter: function (arrayParameter: Array<any>, node: Node) {
        node.childNodes.forEach((childNode) => {
            if (childNode.nodeType == Node.ELEMENT_NODE) {
                const childElement = <Element>childNode;
                const name = childElement.getAttribute("name");
                if (name != null) {
                    if (childElement.getAttribute("hig-property-type") == "Object") {
                        let r = {};
                        arrayParameter.push(r);
                        this.processParameter(r, childNode);
                    }
                    else if (childElement.getAttribute("hig-property-type") == "Array") {
                        let rr = [];
                        arrayParameter.push(rr);
                        this.processArrayParameter(rr, childNode);
                    }
                    else {
                        if (childElement.tagName == "INPUT") {
                            let inputElement = childElement as HTMLInputElement;
                            if (childElement.getAttribute("type") == "checkbox") {
                                if (inputElement.checked == true) {
                                    arrayParameter.push({ name: inputElement.value });
                                }
                            }
                            else {
                                arrayParameter.push({ name: inputElement.value });
                            }
                        }
                        else {
                            arrayParameter.push({ name: childElement.textContent });
                        }
                        this.processParameter(arrayParameter, childNode);
                    }
                }
                else {
                    if (childElement.getAttribute("hig-property-type") == "Object") {
                        let r = {};
                        arrayParameter.push(r);
                        this.processParameter(r, childNode);
                    }
                    else if (childElement.getAttribute("hig-property-type") == "Array") {
                        let rr = [];
                        arrayParameter.push(rr);
                        this.processArrayParameter(rr, childNode);
                    }
                    else {
                        this.processParameter(arrayParameter, childNode);
                    }
                }
            }
        });
    }
});

 

1. Value list from only checked checkbox

HTMX does not support value array from only checked checkbox.

https://github.com/bigskysoftware/htmx/issues/2624

So, I created json-higlabo extension.

You create html like below.

<div id="PermissionValueListPanel">
    <div name="Permissions" hig-property-type="Array">
        <div>
            <input id="Permission_0" type="checkbox" value="Permission_0">
            <label for="Permission_0">Permission_0</label>
        </div>
        <div>
            <input id="Permission_1" type="checkbox" value="Permission_1">
            <label for="Permission_1">Permission_1</label>
        </div>
        <div>
            <input id="Permission_2" type="checkbox" value="Permission_2">
            <label for="Permission_2">Permission_2</label>
        </div>
    </div>
</div>

Add button like below.

<button id="ParsePermissionListButton" 
        hx-trigger="click" hx-include="#PermissionValueListPanel"
        hx-post="/post-data" hx-ext="json-higlabo" 
        hx-target="#PermissionValueListPanelJson" hx-swap="innerHTML">
    Send checked value list
</button>

 

The JSON text 

{
  "Permissions": [
    "Permission_0",
    "Permission_2"
  ]
}

JSON will send to server. You create a C# class like this.

public class HtmxPostData
{
    public List<string> Permissions { get; set; } = new();
}

Now, you can receive input data from browser by mapping to this class. This is C# example, you can also receive input data by python, PHP, or other language.

 

2. Checkbox object array

In other case, you want to get checked or not, and the name and value of checkbox object.

If you add name and value attribute, the checkbox will be a object that has name, value, checked properties.

<div name="PermissionObjects" hig-property-type="Array">
    <div>
        <input id="PermissionObject_0" type="checkbox" name="Permission_0" value="PermissionValue_0">
        <label for="PermissionObject_0">Permission_0</label>
    </div>
    <div>
        <input id="PermissionObject_1" type="checkbox" name="Permission_1" value="PermissionValue_1">
        <label for="PermissionObject_1">Permission_1</label>
    </div>
    <div>
        <input id="PermissionObject_2" type="checkbox" name="Permission_2" value="PermissionValue_2">
        <label for="PermissionObject_2">Permission_2</label>
    </div>
    <div>
        <input id="PermissionObject_3" type="checkbox" name="Permission_3" value="PermissionValue_3">
        <label for="PermissionObject_3">Permission_3</label>
    </div>
    <div>
        <input id="PermissionObject_4" type="checkbox" name="Permission_4" value="PermissionValue_4">
        <label for="PermissionObject_4">Permission_4</label>
    </div>
</div>

Below object will be create.

{
  "PermissionObjects": [
    {
      "Name": "Permission_0",
      "Value": "PermissionValue_0",
      "Checked": true
    },
    {
      "Name": "Permission_1",
      "Value": "PermissionValue_1",
      "Checked": false
    },
    {
      "Name": "Permission_2",
      "Value": "PermissionValue_2",
      "Checked": true
    },
    {
      "Name": "Permission_3",
      "Value": "PermissionValue_3",
      "Checked": false
    },
    {
      "Name": "Permission_4",
      "Value": "PermissionValue_4",
      "Checked": false
    }
  ]
}

You can map JSON to below C# class.

public class HtmxPostData
{
    public List<PermissionSettings> PermissionObjects { get; set; } = new();
}
public class PermissionSettings
{
    public string Name { get; set; } = "";
    public string Value { get; set; } = "";
    public bool Checked { get; set; }
}

 

3. Object array that has properties

In business application, you want to handle object array that has multiple properties.

You can use hig-property-type="Object" to handle this case. At first, you must add hig-property-type="Object" attribute to the element that represent object.

<div name="Persons" hig-property-type="Array">
    <div hig-property-type="Object">
        <input type="text" name="Name" value="Name_0">
        <input type="text" name="Age" value="20">
        <input type="checkbox" name="IsAdmin" checked="checked">
    </div>
    <div hig-property-type="Object">
        <input type="text" name="Name" value="Name_1">
        <input type="text" name="Age" value="21">
        <input type="checkbox" name="IsAdmin">
    </div>
    <div hig-property-type="Object">
        <input type="text" name="Name" value="Name_2">
        <input type="text" name="Age" value="22">
        <input type="checkbox" name="IsAdmin">
    </div>
    <div hig-property-type="Object">
        <input type="text" name="Name" value="Name_3">
        <input type="text" name="Age" value="23">
        <input type="checkbox" name="IsAdmin">
    </div>
    <div hig-property-type="Object">
        <input type="text" name="Name" value="Name_4">
        <input type="text" name="Age" value="24">
        <input type="checkbox" name="IsAdmin">
    </div>
</div>

Add button to html.

<button id="ParsePermissionListButton" 
    hx-trigger="click" hx-include="#PersonListPanel" 
    hx-post="/post-data" hx-ext="json-higlabo" 
    hx-target="#PersonListPanelJson" hx-swap="innerHTML">
    Send object list as json array property
</button>

Now, you can get object array JSON.

{
  "Persons": [
    {
      "Name": "Name_0",
      "Age": 20,
      "IsAdmin": true
    },
    {
      "Name": "Name_1",
      "Age": 21,
      "IsAdmin": false
    },
    {
      "Name": "Name_2",
      "Age": 22,
      "IsAdmin": true
    },
    {
      "Name": "Name_3",
      "Age": 23,
      "IsAdmin": false
    },
    {
      "Name": "Name_4",
      "Age": 24,
      "IsAdmin": false
    }
  ]
}

You can get it with C# below class.

public class HtmxPostData
{
    public List<Person> Persons { get; set; } = new();
}
public class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public bool IsAdmin { get; set; }
}

 

4. Recursive object

Recursive object also supported by json-higlabo extension.

You may create department tree from database data like below.

<div id="RecursivePanel">
    <input type="text" name="Name" value="Tech Company, Inc." />
    <div name="Departments" hig-property-type="Array">
        <div hig-property-type="Object" class="department">
            <input type="text" name="Name" value="Software Development" />
            <div name="Departments" hig-property-type="Array">
                <div hig-property-type="Object" class="department">
                    <input type="text" name="Name" value="Frontend" />
                    <div name="Departments" hig-property-type="Array">
                        <div hig-property-type="Object" class="department">
                            <input type="text" name="Name" value="React" />
                        </div>
                        <div hig-property-type="Object" class="department">
                            <input type="text" name="Name" value="HTMX" />
                        </div>
                    </div>
                </div>

                <div hig-property-type="Object" class="department">
                    <input type="text" name="Name" value="Backend" />
                    <div name="Departments" hig-property-type="Array">
                        <div hig-property-type="Object" class="department">
                            <input type="text" name="Name" value="SQL Server" />
                        </div>
                        <div hig-property-type="Object" class="department">
                            <input type="text" name="Name" value="MySQL" />
                        </div>
                        <div hig-property-type="Object" class="department">
                            <input type="text" name="Name" value="Cosmos DB" />
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

You can nest hig-property-type="Object". json-higlabo extension process these attribute and element properly and recursively.

{
  "Name": "Tech Company, Inc.",
  "Departments": [
    {
      "Name": "Software Development",
      "Departments": [
        {
          "Name": "Frontend",
          "Departments": [
            {
              "Name": "React"
            },
            {
              "Name": "HTMX"
            }
          ]
        },
        {
          "Name": "Backend",
          "Departments": [
            {
              "Name": "SQL Server"
            },
            {
              "Name": "MySQL"
            },
            {
              "Name": "Cosmos DB"
            }
          ]
        }
      ]
    }
  ]
}

You can get this JSON in server by creating C# class like this.

public class Department
{
    public string Name { get; set; } = "";
    public List<Department>? Departments { get; set; } 
}

 

Conslutoin

HTMX is so cool and easy to use. This time, I created extension and experienced easy to extend.

In business application, the case like handle multiple records or recursive object are often required. I think json-higlabo will help such case. 

 

If you find a bug or have a feature request, please create issue at 

https://github.com/higty/higlabo

 

History

2024.9.27 Initial post.