Like htmx, but with only one attribute per element - a prototype
Tags: webdev, html, htmx, javascript
Published: 2023-12-16 11:43:54

## Introduction
The [htmx](https://htmx.org/) 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](https://htmx.org/) 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:
```html
<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

```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

```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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://developer.mozilla.org/en-US/docs/Web/API/Element#events) are used.

automata.js
```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
```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
```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

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".

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8ctau2mb61losy7evsr.png)

## 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](https://github.com/mrhdias/secutio/tree/main/prototypes/fresh-fruits)

Thank you for reading!