VaporHX is a collection of Htmx and other extensions that I made when started working with htmx in a personal project (and thus they can be quite opinionated). In any case, please feel free to discuss any changes, as I am open to new and convincing ideas.
The core idea is that you can combine your existing API endpoints with HTMX endpoints with minimal effort. The response will depend on the value of the request Accept
header and the request method.
All you need to do is to call the hx(template: String)
method on your Content
struct and return its value. It will automatically pick the appropriate response, whether it is JSON encoded data, a full HTML page or an HTMX fragment. When HTML (HTMX) is returned, your content is injected into the specified template as a context.
import VHX
// Do NOT forget to call 'configureHtmx' in your 'configure' method before trying this snippet in your project
// Also, do NOT create a folder called '--page' in your template root without changing the default VHX settings
// as it is used as a prefix for dynamically generated wrapper page templates
// and the custom template provider is the last one to be checked after the default ones are run
// Furthermore, this snippet assumes the default leaf naming conventions as they can be manually overriden
struct MyApi: Content {
let name: String
}
func routes(_ app: Application) throws {
// Combined API and HTMX endpoint
// 'api.leaf' template must exist
app.get("api") { req in
MyApi(name: "name").hx(template: "api")
// The return type of this function call is 'HX<MyApi>'
}
// HTMX only endpoint
// 'index.leaf' template must exist
// It will automatically select whether to return a full page or only a fragment if the generic page template was configured.
// Otherwise, it will simply render the 'index.leaf' template
app.get { req in
try await req.htmx.render("index")
}
}
Basic configuration (configure.swift
):
import Vapor
import VHX
// Assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// Generates a dynamic template that wraps the content of the specified template
// This is the template that will be returned when a standard html GET request is made to an htmx endpoint.
// The default implementation returns '#extend("\(name)")'
func pageTemplate(_ template: String) -> String {
"""
#extend("index-base"): #export("body"): #extend("\(template)") #endexport #endextend
"""
}
public func configure(_ app: Application) async throws {
try configureHtmx(app, pageTemplate: pageTemplate)
}
Here is my hot take: Make your backend code the single source of truth for your project, and drop most of your front end bloat in favour of updating your HTML in-place and seamlessly. Without reloading the page and with only your server side HTML templates. Learn more at htmx.org.
And here is the official intro:
- Why should only
<a>
and<form>
be able to make HTTP requests?- Why should only
click
&submit
events trigger them?- Why should only
GET
&POST
methods be available?- Why should you only be able to replace the entire screen?
By removing these arbitrary constraints, htmx completes HTML as a hypertext.
Lastly, here is a quick introduction to HTMX by Fireship
: htmx in 100 seconds.
SPM installation:
- Add the package to your package dependencies
.package(url: "https://github.com/RussBaz/VaporHX.git", from: "0.0.8"),
- Then add it to your target dependencies
.product(name: "VHX", package: "VaporHX"),
Assuming the standard use of configure.swift
:
// The most straightforward configuration
import Vapor
import VHX
// Defining the page dynamic template generator separately
// Check the 'HXBasicLeafSource' later in this section for further details
func pageTemplate(_ template: String) -> String {
"""
#extend("index-base"): #export("body"): #extend("\(template)") #endexport #endextend
"""
}
public func configure(_ app: Application) async throws {
// Other configuration
// HTMX configuration also enables leaf templating language
try configureHtmx(app, pageTemplate: pageTemplate)
// Later configuration and routes registration
}
Here are all the signatures:
func configureHtmx(_ app: Application, pageTemplate template: ((_ name: String) -> String)? = nil) throws
// or
func configureHtmx(_ app: Application, configuration: HtmxConfiguration) throws
// This struct stores globally available (through the Application) htmx configuration
struct HtmxConfiguration {
var pageSource: HXLeafSource
// A header name that will be copied back from the request when HXError is thrown
// The header type must be UInt, otherwise 0 is returned
// Should be used by the client when retrying
var errorAttemptCountHeaderName: String?
// Possible ways to init the configuration structure
init()
init(pagePrefix prefix: String)
init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping (_ name: String) -> String)
init(pageSource: HXLeafSource, errorAttemptCountHeaderName: String? = nil)
}
HXLeafSource
is used to generate a dynamic template that is used for wrapping HTMX fragments with the rest of the page content when it is accessed through a normal browser request.
It satisfies the following protocol:
protocol HXLeafSource: LeafSource {
var pagePrefix: String { get }
}
Where LeafSource
is a special Leaf
protocol designed for customising how leaf templates are discovered.
Then VaporHX implements its implementation of this specialised protocol.
struct HXBasicLeafSource: HXLeafSource {
let pagePrefix: String
// This is our custom template generator
let pageTemplate: (_ name: String) -> String
}
In order to manually initialise this struct, please use the following function:
func hxPageLeafSource(prefix: String = "--page", template: ((_ name: String) -> String)?) -> HXLeafSource
In our case the default pagePrefix
value is --page
. Therefore, everytime you ask leaf
for a template prefixed with --page/
(please do not miss /
after the prefix, it is always required), the default HXBasicLeafSource
will return a template generated by the pageTemplate
closure. Everything after the prefix with /
will be passed into the page template generator and the result of this function should be a valid leaf
template as a string.
The value passed to the pageTemplate
method must not be empty. If it is, then HXBasicLeafSource
will return a 'not found' error.
Lastly, this LeafSource
implementation is registered as a last leaf source, and this means that the default search path is fully preserved.
To be continued...