Introduction
The htmx is a great tool that provides access to AJAX, CSS transitions, WebSockets, and server-sent events directly in HTML. It accomplishes this by using attributes such as hx-get, hx-post, hx-trigger, etc. within HTML tags. However, in certain scenarios, the use of many attributes in the same element can contribute to element pollution. Picture a train of attributes within the same element.
The Prototype
The concept behind this prototype is to apply the same principle as htmx but with a simplified approach, utilizing only one attribute (data-tasks) like the class attribute. The class attribute is often used to point to a class name in a style sheet. This prototype also uses an attribute but to perform tasks that are defined in a json file, to mimic the styles file used by classes.
We can imagine the "data-tasks" attribute like a class of css, but instead of styling the elements styles the actions?
Let's create a simple example to elucidate the concept. Essentially, it involves a button that, upon being clicked, triggers an action on the server, returning a html list. The list includes a button at the end to clear it and restore the initial state.
Example:
<div id="bag">
<button data-tasks="get-fruits" class="fruits-button">
Get Fresh Fruits
</button>
</div>
The idea is that one or more tasks can be defined in an element, similar to classes, where one or more blocks can be defined within an element.
<element data-tasks="task-1 task-n" class="block-1 block-n"></element>
styles.css
.fruits-button {
color: #37474F;
font-weight: bold;
background-color: #fff;
border: 1px solid #000;
cursor: pointer;
}
.fruits-button:hover {
background-color: #c0c0c0;
}
To "stylize" the actions, we will use a JSON file because it is simple to define blocks with the values of the properties (key/value).
tasks.json
{
"get-fruits": {
"action": "/getfruits",
"method": "get",
"target": "#bag",
"swap": "innerHTML",
"trigger": "click"
},
"empty-bag": {
"action": "/emptybag",
"method": "get",
"target": "#bag",
"swap": "innerHTML",
"trigger": "click"
}
}
To implement the idea, I created a basic JavaScript library named "automata.js" with a class called "Automata". This library reads a json file containing tasks and executes them.
The crucial component is the MutationObserver interface, which enables the monitoring of changes to the DOM tree when HTML fragments are inserted or removed.
For testing purposes only a few events are used.
automata.js
/* <![CDATA[ */
class Automata {
constructor(parameters = {
"attribute": "data-tasks",
"json_file": "tasks.json"
}) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element#events
// Some events to test
this.triggers = new Set([
'click',
'mouseenter',
'mouseover',
'mouseup',
'mouseleave'
]);
this.dataAttribute = parameters["attribute"];
this.jsonFile = parameters["json_file"];
this.tasks = {};
}
async makeRequest(data = {}) {
const response = await fetch(data.action, {
method: data.method,
cache: "no-cache"
});
const fragment = await response.text()
return fragment;
}
async getDefinedTasks() {
const response = await fetch(this.jsonFile, {
cache: "no-cache"
});
this.tasks = await response.json();
}
setTask(element) {
const elementTasks = element.dataset.tasks.split(/ +/);
let _this = this;
elementTasks.forEach(function (task) {
if (_this.tasks.hasOwnProperty(task)) {
// console.log("Task:", task);
const parameters = _this.tasks[task];
// the default trigger for Button is "click"
if (!(parameters.hasOwnProperty('trigger') &&
_this.triggers.has(parameters.trigger)) &&
(element.nodeName === 'BUTTON' ||
(element.nodeName === 'INPUT' && element.type === 'button'))) {
parameters.trigger = 'click';
}
element.addEventListener(parameters.trigger, (event) => {
_this.makeRequest(parameters).then((data) => {
if (parameters.hasOwnProperty('target')) {
// console.log(data);
if (parameters.target === 'this') {
if (parameters.hasOwnProperty('swap') &&
parameters.swap === 'outerHTML') {
element.outerHTML = data;
} else {
element.innerHTML = data;
}
} else {
if (parameters.hasOwnProperty('swap') &&
parameters.swap === 'outerHTML') {
document.querySelector(parameters.target).outerHTML = data;
} else {
document.querySelector(parameters.target).innerHTML = data;
}
}
}
});
}, false);
}
});
}
search4Tasks(parentNode) {
const elemWithTasks = parentNode.querySelectorAll("[" + this.dataAttribute + "]");
if (elemWithTasks.length > 0) {
let _this = this;
elemWithTasks.forEach(function (element, index) {
// console.log(element.dataset.tasks);
_this.setTask(element);
});
}
}
findTasksRecursively(addedNode) {
if (addedNode.nodeType !== Node.TEXT_NODE &&
addedNode.nodeType !== Node.COMMENT_NODE) {
for (const node of addedNode.childNodes) {
if (node.hasChildNodes) {
this.findTasksRecursively(node);
}
}
if (addedNode.hasAttribute(this.dataAttribute) &&
addedNode.getAttribute(this.dataAttribute) !== "") {
this.setTask(addedNode);
}
}
}
init() {
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
const targetNode = document.getElementsByTagName('body');
const config = {
childList: true,
subtree: true
};
const callback = (mutations, observer) => {
mutations.forEach(mutation => {
if (mutation.type === "childList") {
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === Node.TEXT_NODE ||
addedNode.nodeType === Node.COMMENT_NODE) {
continue;
}
if (addedNode.hasChildNodes) {
this.findTasksRecursively(addedNode);
continue;
}
if (addedNode.hasAttribute(this.dataAttribute) &&
addedNode.getAttribute(this.dataAttribute) !== "") {
this.setTask(addedNode);
}
}
for (const removedNode of mutation.removedNodes) {
if (removedNode.childNodes.length > 0 &&
removedNode.hasAttribute(this.dataAttribute)) {
console.log("Unimplemented removeEventListener...");
}
}
if (mutation.target.childNodes.length === 0) {
observer.disconnect();
}
}
});
}
const observer = new MutationObserver(callback);
observer.observe(targetNode[0], config);
this.getDefinedTasks().then(() => {
this.search4Tasks(targetNode[0]);
});
}
}
// const automata = new Automata();
// automata.init();
/* ]]> */
Now, a basic HTML page.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-control" content="no-cache">
<meta name="author" content="Henrique Dias">
<link rel=stylesheet href="styles.css">
</head>
<body>
<div id="bag">
<button data-tasks="get-fruits" class="fruits-button">
Get Fresh Fruits
</button>
</div>
<script src="automata.js"></script>
<script>
/* <![CDATA[ */
const automata = new Automata();
automata.init();
/* ]]> */
</script>
</body>
</html>
Finally, we need a web server to make this work, in this case I coded it in Golang, but it could be in another programming language.
server.go
package main
import (
"fmt"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
)
func openUrlInBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func getFruits(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, "<strong>%s</strong><ul>%s</ul><button data-tasks=\"empty-bag\" class=\"fruits-button\">Empty Bag</button>",
"Bag contents:",
strings.Join(func() []string {
list := []string{}
for _, fruit := range []string{
"Orange",
"Apples",
"Pears",
"Pineapple",
} {
list = append(list, fmt.Sprintf("<li>%s</li>", fruit))
}
return list
}(), "\r\n"))
}
func emptyBag(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, "<button data-tasks=\"get-fruits\" class=\"fruits-button\">Get Fresh Fruits</button>")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/getfruits", getFruits)
mux.HandleFunc("/emptybag", emptyBag)
mux.Handle("/", http.FileServer(http.Dir("public/")))
// go openUrlInBrowser("http://localhost:8080")
go func() {
<-time.After(100 * time.Millisecond)
openUrlInBrowser("http://localhost:8080")
}()
server := http.Server{
Addr: ":8080",
Handler: mux,
}
fmt.Printf("Server listening on %s", server.Addr)
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
In the end, the directory structure should be as follows:
- fresh-fruits/
- public/
- automata.js
- index.html
- styles.css
- tasks.json
- server.go
- public/
To perform the test, follow these simple steps: navigate to the root of the prototype directory and execute the following command:
$ go run .
Server listening on :8080
The web page will open automatically in your default browser. Alternatively, you can manually enter the address http://localhost:8080 to access it. Once on the page, you should see a button with the text "Get Fresh Fruits".
Conclusion
This is merely a basic prototype with ample room for improvement. However, I believe it presents a straightforward alternative to appending a lot of attributes to HTML elements.
The source code for this prototype is available here: Automata
Thank you for reading!
Top comments (1)
Repository Title:
"Tech Hub: Curating Top GitHub Projects"
Message:
Hello everyone!
I've created a new GitHub repository hosting a diverse set of top tech projects. It's a hub for innovation and knowledge exchange.
Project Link:
RepoLand - Tech Hub
Features:
How to Contribute:
Invitation:
I invite you to participate and follow the repository. This project serves as a platform for innovation and skill development. Feel free to share this invitation with anyone you think might benefit and contribute positively.
Thank you for your support and future contributions!