Template Manager for htmX (TMaX)
This is simple library to solve the HTMX specific issue regarding cold access
to pushed url.
Now let's imagine you have a index
page that has button to navigate to a content
. You're using hx-get
to transition to that content
.
Now if the user chooses to refresh the page, you will be back to the index
without the content
part of the page. Let's assume you do not want that because it will be a bad user experience.
You want if user refresh the page, they will be still in the content
part of the UI.
HTMX gives us helpful tools such as hx-push-url
to push our hx-get
url towards browser history. Now, even if user refresh the page, it will still accessing the content
part of the view.
But now you have bigger problem.
Let's see what's inside index
page:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body id="root-body">
<p>This is Index body</p>
<button hx-get="/content" hx-target="#root-body" hx-push-url="true">
Navigate to Content
</button>
</body>
</html>
And then let's see what's inside content
part:
<p>This is content body</p>
<button hx-get="/" hx-target="#root-body" hx-push-url="true">
Back to index
</button>
We immediately see that content
does not looks as complete as index
. It was not a valid HTML, it lacks head, it lacks body, etc.
Which is understable, as we want content
as some sort of partials that can be cheaply sent towards the user. But now because we push the url
towards browser history, we let user access this incomplete partials.
We can solve this too by checking whether the request was triggered by HTMX or not. If it was sent by HTMX, let's send a partials, and if it was not triggered by HTMX, let's send a full page with the partials inside it.
To see whether a request were triggered by HTMX, we could just check whether HX-Request
header exist or not. Easy.
And to solve the page partials, we could make 2 separate .html
file that represent the partials and the full page.
But then we need to introduce 2 files, one for the component only, and the other for the composite for a page. This is fine but there must be neater way to do this.
This library solve that issue by compositing the view via convention. First, it requries root html that represent the "Page" state:
{{define "root"}}
<!DOCTYPE html>
<html lang="en">
<head>
{{template "headers"}}
{{template "head" .}}
</head>
<body id="root-body">
{{template "body" .}}
</body>
</html>
{{end}}
Then we can define a component that satisfy the root
precondition (look for "head" and "body" component):
{{define "head"}}
<title>mwyndham.dev Index</title>
{{end}}
{{define "body"}}
<p>This is Index body</p>
<button hx-get="/content" hx-target="#root-body" hx-push-url="true">
Navigate to Content
</button>
{{end}}
This way, you can call the component state by it's name:
return c.Render(http.StatusOK, "index", nil)
or by it's root state:
return c.Render(http.StatusOK, "root#index", nil)
The function itself is look like this:
func NewEchoTemplateRenderer(e *echo.Echo, rootName, viewName, componentPath string, viewPaths ...string)
Where each params is:
e
-> instance of Echo ServerrootName
the name definition of the root layout. For example, if the root layout template is defined as "root" then you should name it "root" too.viewName
is the name definition of the component. In the example above, the component is defined as "body", so we put the value "body" here.componentPath
is the directory where we put all of the non-page composable components (including the root.html). It capable to recursively looking for html file so you can arrange the files as you liked as long as they have common parent which you specify here.viewPaths
is variadic value where you can put one or more path for your page-composable components.
The full example for those function would be like this:
tmax.NewEchoTemplateRenderer(e, "root", "body", "public/components", "public/views") // Don't forget that the last one is variadic