bagasjs

I made a new Javascript Framework (clickbait)

It's kinda weird talking about Javascript Framework in the time where AI is the current hype. But I don't care, because I just want to share what I find when working with the web recently.

Recently, I finally got a job. But for the earlier months, I spent too much of my salary. I am thinking what should I do? Well I should record my cash flow. But I don't trust any application to keep my financial information. So I decided to make my own book keeping application. Since I didn't wanna think too much about it and I want it to be available in my phone, I just straightly choose to build it in the web. But I love small software, thus I won't be using any of the web frameworks.

I called it Yuuka, it's just have 2 file the index.html and the index.js. At first I tried to just make it a static HTML and hide some of the menus. But I quickly ended up wanting to have a dynamic UI. Especially for the manual journal form. Now, usually without any of the modern web framework you will need to wrangle with document.createElement for constructing the DOM element. But it's too sucks because now building the UI is not declarative just like how I do it in the HTML. Then, I remembered that one of streamer that I watched Tsoding have a pretty cool library or rather a way to declaratively do UI in the web without all the modern web framework which is grecha.js. I've used this technique a fair amount of times in the past for working with my web project.

So what's the technique proposed by grecha.js? It's basically just this few lines of code


function tag(name, ...children) {
    const result = document.createElement(name);
    for (const child of children) {
        if (typeof(child) === 'string') {
            result.appendChild(document.createTextNode(child));
        } else {
            result.appendChild(child);
        }
    }

    result.att$ = function(name, value) {
        this.setAttribute(name, value);
        return this;
    };

    result.onclick$ = function(callback) {
        this.onclick = callback;
        return this;
    };

    return result;
}

It's so simple right? It's basically the builder pattern that you embed/extend to the already existing DOM element object. Now you can easily do something like this


tag(
    "ol",
    tag("li", "apple")
    tag("li", "orange")
)

Well, but I kinda want more so I usually do something like this. Because usually, you want to add children to an element dynamically not only at the element construction time.


function el$(tag) {
    const result = document.createElement(tag);
    result.add$ = function(...items) {
        for(const item of items) {
            if(typeof item === "string") {
                this.appendChild(document.createTextNode(item))
            } else {
                this.appendChild(item)
            }
        }
        return this;
    }
    result.att$ = function(key, value) {
        this.setAttribute(key, value)
        return this
    }
    result.onclick$ = function(callback) {
        this.onclick = callback
        return this;
    }
    return result;
}

But here's the thing, it's still not ergonomic and clean. Consider the following example


const fruits = ["Apple", "Banana", "Orange"]
const listEl = el$("ol")
for (const fruit of fruits) {
    const normalized = fruit.trim().toLowerCase()
    const label = normalized[0].toUpperCase() + normalized.slice(1)
    const item = el$("li")
        .att$("data-fruit", normalized)
        .add$(label)

    listEl.add$(item)
}
document.body.appendChild(listEl)

It's kinda sucks imagine if I have to add another list e.g. animals. I can't just copy all those statements because then I will have duplicate variables listEl and now I have to find another name for listEl. Now, I know that you can inline it into two statement by using .forEach method but my brain doesn't get to that conclusion at the moment. But I think I come up to a better solution for this ergonomicity problem that I think ended up making this utility like 10x times more useful. You basically just need to add .body$ method


function el$(tag) {
    const result = document.createElement(tag);
    result.add$ = function(...items) {
        for(const item of items) {
            if(typeof item === "string") {
                this.appendChild(document.createTextNode(item))
            } else {
                this.appendChild(item)
            }
        }
        return this;
    }
    result.att$ = function(key, value) {
        this.setAttribute(key, value)
        return this
    }
    result.onclick$ = function(callback) {
        this.onclick = callback
        return this;
    }
    result.body$ = function(callback) {
        callback(this);
        return this;
    }
    return result;
}

What .body$ method do is basically just call a function that pass that element that call it into that callback. Well that sentence is tounge twister. But what basically .body$ enables is now you can do this.


const fruits = ["Apple", "Banana", "Orange"]
document.body.appendChild(el$("ol").body$(listEl => {
    for (const fruit of fruits) {
        const normalized = fruit.trim().toLowerCase()
        const label = normalized[0].toUpperCase() + normalized.slice(1)
        const item = el$("li")
            .att$("data-fruit", normalized)
            .add$(label)

        listEl.add$(item)
    }
}))

Doesn't that feels more ergonomic? You can still do imperative programming when you need to while having the access to that element you want to build. Instead of using .forEach, I think this is a better solution. Moreover, this technique has a side effect. Look at the following.


document.body.appendChild(el$("form").body$(form => {
    const items = el$("ol")
    form.add$(items)
    let i = 0;
    form.add$(el$("button").onclick$(_ => {
        items.add$(el$("li").add$("hello, ", i)
        i += 1
    })
})

Can you see it? now you suddenly can create a dynamic UI. And not only that, you also have a local state for that element, and also easily accessing other siblings element while building other element. This is happened because all those state is actually stored automatically as a closure of that callback. While this might impact your memory usage because closure is not free, I think for a small website this is acceptable. I would even use this for some of my production website (if I have to do web in the future) because even though it's stupid at least the idea is simple enough that you can just inspect it later.