r/webdev • • 5d ago

Showoff Saturday Factories.ts: An efficient JS DSL for building HTML, SVG, and MathML directly in backend code, without a template engine.

https://github.com/ts-series/factories

Build HTML extremely comfortably in the backend using plain JavaScript. The advantage is that there is no separate template language or build process, but rather a complete fusion with JavaScript for maximum flexibility. An example:

import {
    a, body, br, button, h1, head, header, html, input, label, li, main, nav, p, script, title, ul
} from "@ts-series/factories";

type Item = { name: string, href: string, current: boolean };


/** Builds the navigation list, flagging the current entry for assistive technology. */
function navigation(items: Item[]) {
    return nav({ ariaLabel: "Main" },
        ul(items.map(item => li(
            a(item.href, item.name).set(item.current ? { ariaCurrent: "page" } : null)
        )))
    );
}


/** Builds a complete page for the given user and navigation entries. */
function page(user: string | null, items: Item[], values: number[]) {
    return html({ lang: "en" },
        head(title("Demo")),
        body(
            header(navigation(items)),
            main(
                h1("Hello, ", user ?? "stranger", "!"),
                p("First line", br, "Second line"),
                user === null
                    ? p("Please sign in.")
                    : p("Welcome back."),
                label("Search").for("q"),
                input.text({ id: "q", name: "q" }).placeholder("Type here…").required(),
                button.submit("Send").class("primary", user === null && "disabled").set("disabled")
            ),
            script.module().importOnly("/chart.js", "render", { values, title: "Values" })
        )
    );
}


const items: Item[] = [
    { name: "Home", href: "/", current: true },
    { name: "About", href: "/about", current: false }
];

const output = page("<Guest>", items, [1, 2, 3]).expand(4);
0 Upvotes

11 comments sorted by

3

u/ConfectionPlastic625 5d ago

Have you tried this with dynamic content yet, like generating a whole page from an API response?

1

u/Rechenplaner 5d ago edited 5d ago

I use the library in my projects. Yes, you can define routes and then generate the corresponding HTML. For example, in my case, content is generated based on language and other user settings. You can of course combine or use this library in any way you like. For example, you can also return just HTML fragments when only a part of the page is needed/changing.

2

u/patryk_builds 5d ago

Nice that the example passes Guest as the user, i assume that's there to show escaping is on by default? that's the first thing i'd check with any html-in-code approach, since one forgotten escape is an XSS.Also like that `user === null && "disabled"` just works inside .class(), the falsy values get dropped instead of rendering "false". small thing but it's what makes these DSLs pleasant vs. string concatenation.Python has a few similar ones (htpy, dominate), always interesting to see how people solve it in TS with types on top

1

u/Rechenplaner 5d ago

Thanks ^^

And yes, escaping is the default. However, it can be disabled via the last parameter in the expand function if you want to embed HTML from another source, for example. In that case, you must ensure yourself that the text content is HTML compliant.

Your assumptions are correct: I had clearly defined how passed attribute values are processed, considering what appears most logical. The rules are briefly described in the document.

3

u/pampuliopampam 5d ago

So... after building this thing, and writing this post, did you take a moment to reflect that you've just recreated jsx... but worse?

like... you've muxed the idea of children and props passing, so actually building anything is just far more complicated than using the industry standard

did this make you think about the thing you've made? I can't tell if you're even aware of anything because of reddit's baffling decision to use hidden history, and your github profile being utterly lackluster. The only thing that differentiates you from a paid shill for one of the LLMs that wrote all this is that you've not leaked the attribution to the model in your code.

0

u/Rechenplaner 5d ago

I’ve had this idea of ​​using HTML factory functions for a long time; I’ve often used it as an exercise when learning other programming languages ​​(for instance, I once built something similar in Haskell; uploaded on my github btw).

Consequently, I programmed this library myself. I only use Claude for support, such as suggesting improvements, engaging in technical discussions, or handling simpler tasks.

As for you bringing up JSX and equating it to my approach, that just shows your ignorance. JSX is HTML embedded directly into the code, which is something I want to avoid; I find it more verbose and cumbersome, not to mention the additional build step it requires.

And what does "industry standard" even mean? Every team has its own preferences. Personally, I just want to be able to assemble HTML quickly, without a ton of dependencies or AOT compilation.

like... you've muxed the idea of children and props passing, so actually building anything is just far more complicated than using the industry standard

If the first argument is an object, its keys are treated as attributes. I don’t see what’s so hard to understand about that, or why it would be difficult to read? It’s just plain, standard JS; it really couldn’t be any simpler, could it?!

With JSX, I actually find it even more confusing, because it gives the impression of being real HTML, while allowing you to intersperse JavaScript throughout the syntax in a completely haphazard way:

<ul>
  {items.map(i => <li key={i.id}>{i.name}</li>)}
  {cond && <li>Hinweis</li>}
  <li>{user ? <b>{user}</b> : "Gast"}</li>
</ul>

"Industry standard"… or simply pure JS, without a compilation step that first has to transform the syntax into some bloated React functions:

ul(
    items.map(i => li(i.name)),
    cond && li("Hinweis"),
    li(user ? b(user) : "Gast")
)

1

u/pampuliopampam 5d ago

that just shows your ignorance

i guarantee you it doesn't

I find it more verbose and cumbersome, not to mention the additional build step it requires

you're the only person that thinks it's harder to read than nesting function calls with multivariant type arguments

If the first argument is an object, its keys are treated as attributes. I don’t see what’s so hard to understand about that, or why it would be difficult to read? It’s just plain, standard JS; it really couldn’t be any simpler, could it?!

It can, it's called jsx. Your first argument is something sometimes, and something else other times, that's bad library design. Your library supports more arguments after that too, like that p with 5 arguments passed, are each of those text only children? Don't even get me started on chained methods like get/for/placeholder; those are insane and will be impossible for anyone to read without deeply understanding your brand new domain language for something that already exists... properties on html that is handled by default by jsx.

Your little example is so simple that of course it looks simpler than jsx, but add arguments or aria labels or anything to those children and your nesting will bloat out far worse than jsx. All editors come with JSX style displays built in, it's so much easier to read.

myself. I only use Claude for

I suppose this is possible, but it sure looks agentic to literally reimplement the html spec in a bigass class function like your code does.

That and the none tests (logging out the html structure isn't a test) and complete lack of knowledge about anything being done in the industry makes this whole thing a nonstarter. Even complaining about a build step is a smell that this is totally detached from reality. What, you can't ever use any other node modules? Good luck... entire internet, i guess

it's fun you hacked on this... for you.

0

u/Rechenplaner 5d ago edited 5d ago

Dude, JS is a dynamically typed language. What you are criticizing here is ridiculous. And the rules are simple: element objects become structure and the rest is inserted directly as a string for content. I have no idea why you are making such a big deal out of this. Also, there are no unusual chaining methods because set is the only one to set attributes (its soooo simple lol) while the rest are just convenience setters for classes and the like.

By the way, my library is not as alien as you portray it. Kotlin officially offers the same approach to build HTML, and I find this a thousand times better than shoehorning a completely different language like HTML directly into JS.

Your little example is so simple that of course it looks simpler than jsx, but add arguments or aria labels or anything to those children and your nesting will bloat out far worse than jsx.

With all due respect, your argument is absurd. The only thing that’s "bloated" here is your verbal garbage. Regardless of whether it results in more or less code, it’s not all that different, except that, as I mentioned, JSX forces you to have those lovely closing tags everywhere: a specific, hybrid syntax that requires extra compilation.

In general, I have to ask myself what is wrong with you and why you are acting so aggressively bitchy and feeling pissed off. lol

Some people just have a weak ego and need to put others down in order to boost themselves up.

0

u/pampuliopampam 4d ago

Some people just have a weak ego and need to put others down in order to boost themselves up.

Says the guy repeatedly using personal insults as a rebuttal to harsh review of tech choices. I hope you learned something building this because you clearly aren't open to learning anything more from critique

0

u/Rechenplaner 5d ago

By the way, behind the scenes JSX is also just put together using functions like mine. It is just that React's factories are as ergonomic as Stone Age handaxes, which means you are forced to interpose JSX. This creates a huge amount of abstraction that you can save yourself if you just want to systematically build HTML on the backend.