bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
35.75k stars 1.21k forks source link

Appending urls #1667

Open maekoos opened 11 months ago

maekoos commented 11 months ago

TL;DR: Is there a way to simply append to the current url in htmx? It would be nice in order to avoid ugly backend templating.

The problem

I have recently started using htmx on a project. It is the perfect fit, in combination with a custom html template function (html\``) and oak.

The way most people do backend routing, in my experience, always looks a bit like this:

app.get('/item/:itemId') // show item details

However, in htmx each page almost always includes at least a couple of extra compjnents (e.g. in-place editing and search functionality). Each "component" often requires at least a GET and a POST:

app.get('/item/:itemId') // show item details
app.get('/item/:itemId/edit-item') // get the item editor
app.post('/item/:itemId/edit-item') // or maybe a patch on /item/:itemId

This works fine, but when we want to include this in the page we always have to include the id. Let's say the above routes returns the following html:

// /item/:itemId:
<div hx-target="this">
    <h1>${item.name}</h1>
    <button hx-get="/item/${item.id}/edit-info">Edit</button>
</div>

// /item/:itemId/edit-info
<form hx-post="/item/${item.id}/edit-info">...</form>

Now, all of the urls can probably be simplified to ./${item.id}/... which is better, but I feel like there is a better way which does not include the extra templating (which means the edit-info GET request is just static html...). Wouldn't it be nice to be able to append the url, instead of repeating the prefix?

The solution

There are probably a thousand ways this could be implemented, but I feel like something like this would make sense:

<button hx-get="+/edit-info">Edit</button>

<form hx-post="+/edit-info">...</form>
alexpetros commented 11 months ago

Perfectly reasonable suggestion; I think there might be some hesitation about deviating from the HTML URL semantics though. Would love to hear other thoughts.

Telroshan commented 11 months ago

Why not simply use standard URL params here? I understand the benefits from such a URL for links or URLs that the user may directly access. It simply looks better to say, access /product/id or even /article/title-where-you-already-get-some-info-by-just-reading-the-URL

But for "hidden" API requests (hx-post, hx-patch and partial GETs of which the user would never directly access the URL in their browser), what's the point?

Imo it makes for a more complicated syntax to CTRL + F for in a project, because your URLs are suddenly cut in half with some parameter. For instance, it would be easier to CTRL + F for /item/edit-info itself inside the full URL /item/edit-info?id=${item.id} in a template, wheras if it were a /item/${item.id}/edit-info, it suddenly becomes dependent on how you named you local item variable, and isn't a easily searchable, absolute URL anymore

What I mean by this is, is there any other benefit from such a routing system than "looking good" ? In which case, aside from URLs that the end user may see in their browsers, I don't really get the point of it

If you want to avoid passing some variables in your child templates because then you would have to add some data just to pass that item.id again in a template that doesn't care except putting it again in the URL, you may as well use a hidden input + hx-include to retrieve that value without having to include it in your child template

Something like

<div class="parent-context" hx-include="closest .parent-context">
  <input type="hidden" name="itemID" value="${item.id}">
  <div>
    ....
    <div id="somethingToSwap" hx-get="/some-partial-view" hx-target="this" hx-swap="outerHTML"></div>
</div>

Ofc, you could also hx-include only that specific hidden input and not the whole parent-context, but in case you'd like to add more values than just the item ID, you could do it this way

Then if you were to swap the following in place of somethingToSwap, say

<button hx-post="/item/edit-info" hx-target="#somethingElse">Click me</button>

The button's request on click would automatically include the itemID because the parent-context ancestor defines hx-include which is inherited.

This should achieve what you need here (include values without having to render them again in the backend template just to pass the value in some other request), while retaining easily searchable URLs

maekoos commented 11 months ago

Well, I agree that there is little point in having a "good looking" URL if the user will never see it. However, the structure I feel like would make the most sense when using HTMX is having all "components" of a page be sub-routes to that route.

Something like this:

<!-- /foo/some-foo-id -->
<div>
  <h1>Bars in this foo:</div>
  <ul hx-get="/foo/some-foo-id/bars-list" hx-swap="innerHTML" hx-target="this" hx-trigger="load"></ul>
</div>

<!-- /foo/some-foo-id/bars-list -->
<li>Bar 1 <button hx-delete="/foo/some-foo-id/bar/some-bar-id" hx-target="this" ...>Remove<button> </li>
<li>Bar 2 <button hx-delete="/foo/some-foo-id/bar/some-bar-id" hx-target="this" ...>Remove<button> </li>
<li>Bar 3 <button hx-delete="/foo/some-foo-id/bar/some-bar-id" hx-target="this" ...>Remove<button> </li>

Writing the template for /foo/some-foo-id now requires the foo id in the url. It may not seem significant, but I find a lot of the pages I make include at least two "levels" of components. In this example, the remove button also requires the foo id, instead of something like hx-delete="+/bar/some-bar-id". If there would have been another sub-component it could then append again: hx-post="+/confirm", instead of including the whole URL: hx-post="/foo/some-foo-id/bar/some-bar-id/confirm".

The input method @Telroshan mentioned may work, but in my opinion it creates spaghetti code for very little reason. Maybe there is some other html-native way of achieving this?

Telroshan commented 11 months ago

I see your point. I guess it's a matter of preference here!

I prefer to have explicit URLs precisely to avoid that spaghetti code you're mentioning. To me, the solution you suggest requires to be aware of the parent context, which involves some guessing / looking around to find out what a button is actually doing (or which endpoint it's actually hitting). Adding then more nested components means more nested files to crawl up to finally get to the root & understand where a relative URL is finally heading. I understand explicit URLs may look tedious though, so again, to each their own!

I don't know of any html-native way to achieve what you want here, though I'm not omniscient so there could totally be one that I simply don't know about

jeremylowery commented 4 months ago

I think extended the concept of the HTML <BASE> tag would be an interesting idea. It's practically worthless because you can only specify it at the document level. It's double worthless in HTMX. If you could specify a base (let's say via a hx-base attribute) then all of the URL's inside that element would be relative to the base if they were not given as absolute.

I came across this idea when I was trying to make server side components that needed to make links that all "stayed inside" of the component.

<article hx-base="/articles/12345">
     <button hx-get="delete">Delete</button>
     <form hx-post="update"><input type="text" name="name" />...</form>
</article>

If this was taken care of by the library then it could also handle the URL ending slash or not slash problem.

As it stands right now when you do an hx-get that doesn't swap the URL relative URL's are completely useless because the browser creates everything relative to what's in the URL bar, not based on the XHR that populated the element.

HTMX already has the inheritable feature of descendants inheriting their hx attribute so this would be completely in line with the library.

SafEight commented 4 months ago

Can't you just remove the leading slash?

Am I missing something?

maekoos commented 3 months ago

Can't you just remove the leading slash?

  • e.g. if you're on example.com/item/2/, then

    • hx-get="/request" will get example.com/request, but

    • hx-get="request" will get example.com/item/2/request

Am I missing something?

I believe you would have to do '2/request' meaning the "component" would have to be aware of the id. Although this may depend on if you have a trailing slash or not - I'm not 100% sure.

SafEight commented 3 months ago

I believe you would have to do '2/request' meaning the "component" would have to be aware of the id. Although this may depend on if you have a trailing slash or not - I'm not 100% sure.

I can confirm it works the way I described when the url has a trailing slash.