HTMX custom json extension that handle array, records, recursive property
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.