Try the TAP stack if you hate HTML, CSS, and Javascript!
the most unmaintainable frontend code
2024-03-14
Contents🔗
Look At This Todo App🔗
Look at this Todo app:
Pretty nice, right?
Wrong. It is not nice. It is awful. And it is awful on purpose. I wanted to try to build something using as few best practices and web standards as possible. You know. “For fun.”
Introducing the TAP Stack🔗
This is the TAP Stack:
- Tailwind
-
Instead of CSS, write hundreds of inline utility classes!
- https://tailwindcss.com/
- Alpine
-
Instead of Javascript, write minimal, inline directives! It is the tailwind of JS.
- https://alpinejs.dev/
- Pug
-
Instead of HTML, write whitespace-significant markup with no closing tags!
- https://pugjs.org/api/getting-started.html
That’s right! With the TAP Stack, you can now write applications without any HTML, CSS, or Javascript at all!
Separations of concerns? Separate files? Forget about it! Don’t repeat yourself? How about repeat yourself! Modules? Components? Classes? Eh, maybe!1
What Does It Look Like?🔗
That’s the best part! It looks like an indecipherable ball of gnarled twine! It looks like cognative decline made manifest. It looks like that one time I saw some kid vomit spaghetti into the bathroom sink. Just a real mess.
It looks like this:
body.bg-gray-100.font-sans.min-h-screen.flex.items-center.justify-center
.bg-white.p-6.rounded-lg.shadow-lg.w-max(class="min-w-[600px]" x-data="todoApp()")
h1.text-2xl.font-semibold.mb-4.text-center todo
template(x-if="func === 'new'")
.flex.justify-end.items-center.relative.border-solid.border-slate-300.border-2.rounded-md.w-full
input(x-model="input" @keydown.enter="addTodo").py-2.px-3.w-full
i.material-icons add
template(x-if="func === 'search'")
.flex.justify-end.items-center.relative.border-solid.border-slate-300.border-2.rounded-md.w-full
input(x-model="input" @keydown.enter="view = model.filter(({ text }) => text.includes(input))").py-2.px-3.w-full
i.material-icons search
.flex.justify-end.gap-2
a(@click="func = 'search'" :class="(func === 'search') && 'active' || 'inactive'").cursor-pointer Search
a(@click="func = 'new'" :class="(func === 'new') && 'active' || 'inactive'").underline.cursor-pointer New
.flex.mt-4.justify-between.gap-16
span
span(x-text="view.length")
|
span(x-text="(filter === 'all') ? 'total' : (filter === 'complete') ? 'completed' : 'active'")
span.flex.gap-1
a(@click="filter = 'all'; updateView()" :class="filter === 'all' && 'active' || 'inactive'").cursor-pointer All
a(@click="filter = 'active'; updateView()" :class="filter === 'active' && 'active' || 'inactive'").cursor-pointer Active
a(@click="filter = 'complete'; updateView()" :class="filter === 'complete' && 'active' || 'inactive'").cursor-pointer Completed
a(@click="model = model.filter(({ isComplete }) => !isComplete ); updateView()").underline.cursor-pointer Clear Completed
ul.mt-4
template(x-for="({ text, isComplete }, idx) in view")
li.flex.justify-between.items-center.py-2(x-transition)
input(type="checkbox" :checked="isComplete").scale-150.outline-1
input.grow.text-left.cursor-pointer.px-2(x-model="text" @change="Object.assign(model.at(idx), {text}); saveChanges();")
button(@click="model.splice(idx, 1); updateView(); saveChanges();")
i.material-icons closeThanks to pug’s implicit divs, you barely need to write any actual tags! You can just start chaining tailwind utility classes. That’s synergy, baby.
Sprinkle a little x-for, x-if, and
x-model in there, add a little :bind, and top
it off with a couple @events and you’ve got a whole dang
old application!
Inline styles! Inline scripts! It’s web dev from the 1990s! This site is under construction! Sign my guestbook!
Seriously Though🔗
Here are some pros and cons of working the TAP Stack.
Pros:
No context switching. Everything is in one file. If you want that though, just use vue. Or svelte.
You absolutely can write reusable components in pug with mixins. And you can use its “blocks” to create layouts. In fact if you get highly modular, it … might actually be kind of nice to have terse, tight, tiny TAP components?
For tiny components, a lot of setting up simple interactions is just boilerplate. I could actually get behind inline scripting like this for something that just needs to toggle itself on and off for example.
It’s light. You can serve tailwind and alpine from a CDN. You don’t need vite or esbuild or anything to transform pug into html.
Cons:
Lack of semantics. This is not my first time playing with pug + tailwind, so I can tell you from experience that coming back to a codebase that looks like this a couple months later is absolutely mind boggling. It is straight up not legible.
It’s just gross.
Conclusion🔗
I mean. I wouldn’t write anything like this that needed to be maintained long term. But I might actually consider using the TAP Stack for quickly prototyping a minimal viable product. Guarantees that your first draft gets thrown away, so that you can do it right on your second attempt.
So what are you waiting for! Try the TAP Stack today!