blog: htmx post
This commit is contained in:
parent
3bf622200b
commit
47c9cab2ff
BIN
public/img/blog/en/htmx-01/music-player-01.jpg
Normal file
BIN
public/img/blog/en/htmx-01/music-player-01.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
public/img/blog/en/htmx-01/music-player-02.jpg
Normal file
BIN
public/img/blog/en/htmx-01/music-player-02.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
public/img/blog/en/htmx-01/music-player-03.jpg
Normal file
BIN
public/img/blog/en/htmx-01/music-player-03.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
BIN
public/img/blog/en/htmx-01/types-01.jpg
Normal file
BIN
public/img/blog/en/htmx-01/types-01.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@ -136,7 +136,47 @@ hiding buttons in the frontend should be enough.
|
|||||||
|
|
||||||
> Me: 🤨😑🤮🫠
|
> Me: 🤨😑🤮🫠
|
||||||
|
|
||||||
to be continued...
|
## Using whatever feels fast
|
||||||
|
|
||||||
|
Something I've noticed is that, apparently, my colleages don't know
|
||||||
|
any of what is going on with the codebase. Or at the very least, they
|
||||||
|
don't know how it works.
|
||||||
|
|
||||||
|
When implementing a new feature they will do something like:
|
||||||
|
|
||||||
|
- ask chatgpt to implement the feature
|
||||||
|
- copy pasta the code provided by chat gpt
|
||||||
|
- get errors because duh
|
||||||
|
- copy pasta the errors into chatgpt
|
||||||
|
- copy pasta the chatgpt output into the codebase
|
||||||
|
- repeat until feature is done
|
||||||
|
|
||||||
|
And so, I believe that because of this, either they don't really
|
||||||
|
feel the impact of all the complexity, or they don't know it exists,
|
||||||
|
because it is all chat gpt.
|
||||||
|
|
||||||
|
|
||||||
|
## Why no htmx?
|
||||||
|
|
||||||
|
To be expanded on the next blog post.
|
||||||
|
|
||||||
|
|
||||||
|
## In conclusion
|
||||||
|
|
||||||
|
I think that the trend of using React everywhere, and the arrival of
|
||||||
|
chat gpt have doomed "modern" web development. Those 2 things have
|
||||||
|
enabled developers to build a prototype **blazingly fast**, without
|
||||||
|
concerning with efficiency, security, mantainability, etc. Since
|
||||||
|
they can just make chat gpt do their work for them, they don't
|
||||||
|
understand the severity of the choices they make, don't learn
|
||||||
|
about new, better ways to do the same, and continually build
|
||||||
|
bloated software applications.
|
||||||
|
|
||||||
|
It also doesn't help that either they are not able, or willing,
|
||||||
|
to learn new things. Hell, most of my colleages struggle with
|
||||||
|
reading simple error messages, and some can't seem to be able to
|
||||||
|
figure out how typescript or react work.
|
||||||
|
|
||||||
|
So it's not fully chat gpt's fault. But it has enabled those
|
||||||
|
developers to become complacent, and stop improving.
|
||||||
|
|
||||||
|
240
src/pages/blog/en/real-htmx-01.md
Normal file
240
src/pages/blog/en/real-htmx-01.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
---
|
||||||
|
layout: ../../../layouts/BlogLayout.astro
|
||||||
|
title: Why it might be hard to adopt htmx
|
||||||
|
description: |
|
||||||
|
Adopting htmx is not as simple as it seems, specially if your focus is on
|
||||||
|
development speed rather than quality.
|
||||||
|
pubDate: "2024-10-27"
|
||||||
|
tags: ["javascript", "htmx", "hyperscript", "alpine", "demo"]
|
||||||
|
image:
|
||||||
|
url: ""
|
||||||
|
alt: ""
|
||||||
|
caption: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
TLDR: htmx viability is limited by its environment: hyperscript, alpine, etc.
|
||||||
|
|
||||||
|
|
||||||
|
Ever since I was evangelized by the htmx x's (formerly twitter) posts,
|
||||||
|
I wanted to use htmx on a real project. And a CRUD would be the right
|
||||||
|
chance to do so, right?
|
||||||
|
|
||||||
|
However, I found that htmx would not be so easy to adopt. These are not
|
||||||
|
htmx faults, rather, faults with the htmx ecosystem, so to speak.
|
||||||
|
|
||||||
|
These are the things that I found made it difficult to use htmx:
|
||||||
|
|
||||||
|
## Lack of a UI library like shadcn
|
||||||
|
|
||||||
|
htmx is not a UI library. However, it requires the UI to exists.
|
||||||
|
When I tried implementing my CRUDs with plain html, building the UI
|
||||||
|
was a problem. Unless I'm blind, most modern UI libraries focus either
|
||||||
|
on React or other JS frameworks. You get React code, not html+css.
|
||||||
|
|
||||||
|
So, if I want to implement a table, I have to write the html+css
|
||||||
|
from scratch. In react, I can just `npx shadcn add form` and
|
||||||
|
use it immediately. It handles responsiveness, dark-theme,
|
||||||
|
animations, etc. There is no such thing for plain HTML.
|
||||||
|
|
||||||
|
And btw, this is the motivation for one of my new side-projects,
|
||||||
|
[htmxui](https://ui.araozu.dev). I want a CLI that is able to
|
||||||
|
generate UI components like shadcn, but instead of react it's
|
||||||
|
capable of emitting code for any templating language: go temple,
|
||||||
|
rust maud, elixir HEEx, etc.
|
||||||
|
|
||||||
|
|
||||||
|
## Hyperscript is not good enough
|
||||||
|
|
||||||
|
Personally, I like the idea of hyperscript. It allows you to do
|
||||||
|
some light scripting, pairs nicely with htmx and it's DSL, once
|
||||||
|
you get used to it, is easy to use.
|
||||||
|
|
||||||
|
However, 2 things come to main as disadvantages:
|
||||||
|
|
||||||
|
- It has a DSL. It's yet another programming language to learn,
|
||||||
|
and it does't have the best way to debug errors. The average dev
|
||||||
|
can barely handle Typescript, I wonder if they could learn something
|
||||||
|
like this.
|
||||||
|
|
||||||
|
- It's not good enough for anything other than basic scripting.
|
||||||
|
For example, I build a music player for navidrome using only htmx
|
||||||
|
and hyperscript.
|
||||||
|
If every last piece of UI you will render is done on the backend,
|
||||||
|
hyperscript is good. But if you ever want to render UI exclusively
|
||||||
|
on the frontend, hyperscript stops being so useful.
|
||||||
|
|
||||||
|
|
||||||
|
### Alpine doesn't integrate as well with htmx
|
||||||
|
|
||||||
|
|
||||||
|
## Case study: my own music player.
|
||||||
|
|
||||||
|
I built a web player that consumes data from a Navidrome server.
|
||||||
|
It is written with Go+Templ, uses tailwind for styling. I wanted
|
||||||
|
to see if it was possible to build a (relatively) dynamic application.
|
||||||
|
|
||||||
|
The initial pages are all really easy to build. Rendering html
|
||||||
|
on the server without having to have a complete turing machine
|
||||||
|
that handles loading state and error states and all is so
|
||||||
|
refreshing.
|
||||||
|
|
||||||
|
![UI of my music player](/img/blog/en/htmx-01/music-player-01.jpg)
|
||||||
|
|
||||||
|
However, as soon as I began to work on the player itself, many
|
||||||
|
problems appeared.
|
||||||
|
|
||||||
|
The music must play on the client (duh), and so the music player
|
||||||
|
must be handled by client side javascript.
|
||||||
|
|
||||||
|
First, I wanted to have a mini music player. When the user clicks on
|
||||||
|
a song (1), the music player will show it's cover, title, artist, and
|
||||||
|
music controls (2).
|
||||||
|
|
||||||
|
![UI of my music player, pt2](/img/blog/en/htmx-01/music-player-02.jpg)
|
||||||
|
|
||||||
|
Hyperscript is uncapable of doing something like this, easily.
|
||||||
|
Instead, I had to write a `<script>` tag that contained a lot of
|
||||||
|
hyperscript code. It was essentialy imperative DOM manipulation via
|
||||||
|
javascript, but, instead of running `document.getElementById("title-container")` everywhere,
|
||||||
|
I could run `put 'song title' into #title-container`.
|
||||||
|
|
||||||
|
I ended up having a lot of code like this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def playSong(title, artist, albumId, songId)
|
||||||
|
set #music-player-img's @src to "/covers/" + albumId
|
||||||
|
put title into #music-player-title's innerHTML
|
||||||
|
put artist into #music-player-artist's innerHTML
|
||||||
|
|
||||||
|
-- Update icons
|
||||||
|
set $playing to true
|
||||||
|
add .hidden to #play-icon
|
||||||
|
remove .hidden from #pause-icon
|
||||||
|
|
||||||
|
if $sound exists
|
||||||
|
$sound.fade($volume, 0.0, 250)
|
||||||
|
wait 250ms
|
||||||
|
$sound.unload()
|
||||||
|
end
|
||||||
|
|
||||||
|
make a Howl from {
|
||||||
|
src: ['url/to/navidrome/'],
|
||||||
|
html5: true,
|
||||||
|
volume: $volume
|
||||||
|
} called $sound
|
||||||
|
$sound.play()
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, I was basically writing javascript with a fancy DSL, and some DOM
|
||||||
|
helpers. The helpers were nice, but debugging was so frustrating, especialy because
|
||||||
|
the hyperscript compiler's error messages are misleading.
|
||||||
|
|
||||||
|
This was only for playing the song. Now I had to manage the state of the player,
|
||||||
|
implement a playing queue, preload the next song seconds before it played, handle
|
||||||
|
play/pause/next/previous, etc. In the end, the hyperscript code was so large
|
||||||
|
and so unmantainable that I genuinely tought to myself "I wish I could use a
|
||||||
|
JS framework for this part".
|
||||||
|
|
||||||
|
Enter, Alpine.
|
||||||
|
|
||||||
|
### Alpine rewrite
|
||||||
|
|
||||||
|
During this experiment I found that the main thing that hyperscript can't do
|
||||||
|
is UI rendering, of any type. Part of the reason we have a million JS frameworks
|
||||||
|
is that the DOM API is verbose, prone to errors and leads to a lot of code.
|
||||||
|
The main benefit JS frameworks have on this front is **declarative UI rendering**
|
||||||
|
paired with **reactivity**.
|
||||||
|
|
||||||
|
![UI of my music player, pt3](/img/blog/en/htmx-01/music-player-03.jpg)
|
||||||
|
|
||||||
|
Being able to define once how the UI is driven from the state, and then being
|
||||||
|
able to just update the state and let the UI update itself is so helpful.
|
||||||
|
Alpine allows for this pattern, so with it I was able to rapidly build the UI,
|
||||||
|
and focus on the player state itself.
|
||||||
|
|
||||||
|
This required javascript, of course, but this time it was actual javascript,
|
||||||
|
and it didn't concern with UI, only logic and state management. And while
|
||||||
|
this is nice, it showed me another flaw with the htmx model.
|
||||||
|
|
||||||
|
See, now I'm writing javascript, a objectively bad language. And sure, there
|
||||||
|
are a million tools for making the experience better, but I'm writing
|
||||||
|
javascript inside a Go Templ template. I barely have syntax highlighting.
|
||||||
|
I have no type information, no error highlighting, anything. It's functionally
|
||||||
|
plain text.
|
||||||
|
|
||||||
|
And so, errors slip easily. Errors that can be easy to detect and correct
|
||||||
|
with the tools js has available, but can't be used inside the template
|
||||||
|
language.
|
||||||
|
|
||||||
|
So, can't you just write that javascript on it's own `.js` file, and import
|
||||||
|
it? Yeah, but now I'm breaking Locality of Behaviour, have more files to
|
||||||
|
manage and keep track of, and I have to carefully remember to import all
|
||||||
|
my js files.
|
||||||
|
|
||||||
|
But more than that, I can't store those JS files close to where the component
|
||||||
|
is defined. My project has a structure like this one:
|
||||||
|
|
||||||
|
|
||||||
|
```plain
|
||||||
|
music-player
|
||||||
|
├── public/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ ╰── tailwind.css
|
||||||
|
│ ╰── img/
|
||||||
|
│ ╰── memes.webp
|
||||||
|
╰── app/
|
||||||
|
├── player.go
|
||||||
|
├── player.teml
|
||||||
|
├── album.go
|
||||||
|
╰── album.templ
|
||||||
|
```
|
||||||
|
|
||||||
|
All my static files are in a `public` folder. All my `go` and `templ` files
|
||||||
|
are on the `app` folder, I can't put static files there! (no, I won't `embed`
|
||||||
|
those into my binary)
|
||||||
|
|
||||||
|
So if I moved the js into the public folder, functionality would be split
|
||||||
|
across different folders.
|
||||||
|
|
||||||
|
|
||||||
|
## Lack of strong typing
|
||||||
|
|
||||||
|
![Meme of types](/img/blog/en/htmx-01/types-01.jpg)
|
||||||
|
|
||||||
|
Meme stolen from [Aaron Francis video: PHP doesn't suck (anymore)](https://www.youtube.com/watch?v=ZRV3pBuPxEQ).
|
||||||
|
|
||||||
|
I'm a type junkie. I want types. I need types. I need strong typing.
|
||||||
|
|
||||||
|
One thing front-end frameworks get right is that everything is strongly typed
|
||||||
|
(ignoring the fact that typescript types are pretend-types). The abstraction that the
|
||||||
|
framework provides is used, whereas htmx depends heavily on element ids and classes.
|
||||||
|
|
||||||
|
So, say I have a `<form hx-post="..." hx-target="#validation-result">` paired
|
||||||
|
with a `<div id="validation-result" />`, where all validation
|
||||||
|
errors will be shown. If at any point that id is changed, your app is silently
|
||||||
|
broken. And you have no idea why. Now refactoring is dangerous.
|
||||||
|
|
||||||
|
This is mitigated by following Locality of Behaviour, but now instead of it being
|
||||||
|
an automated, compiler enforced validation, it depends on following "best practices",
|
||||||
|
"remembering to do it right", and "hoping bugs will be catched in code review".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## ok but how does that affect my CRUD apps?
|
||||||
|
|
||||||
|
Sure, your CRUD apps don't need to be highly interactive. But, really,
|
||||||
|
any interactivity over basic transitions and string replacement, will
|
||||||
|
become a pain.
|
||||||
|
|
||||||
|
Moreover, seeing that using htmx is not as magical as Carson Gross might
|
||||||
|
sell it, teams may be tempted to stay on the thing they already use.
|
||||||
|
So, I think that for htmx to be used even more, rather than htmx itself
|
||||||
|
improving, it's ecosystem so to speak must also improve.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user