Skip to content

RussBaz/VaporHX

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VaporHX - Swift Vapor + Htmx + Extensions

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.

Core Idea

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)
}

Table of Contents

What is HTMX?

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.

HTMX

Installation

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"),

Configuration

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.

HTMX Request Extensions

To be continued...