Skip to content

Repository files navigation

HTMX

A haiku from the htmx team:

javascript fatigue:
longing for a hypertext
already in hand

…​and from Hypermedia Systems:

The goal of [htmx] is not less JavaScript, but less code, more readable and hypermedia-friendly code.

This gem provides native Ruby support for the htmx JavaScript library so you can build sophisticated web applications using pure Hypermedia REST APIs while avoiding unnecessary bloat and complexity common with the JavaScript ecosystem. By building upon the original foundations of Hypermedia REST APIs, you can build rich web applications with no additional JavaScript!

Features

  • Augments the htmx JavaScript library.

  • Enhances htmx development.

  • Compatible with any web framework.

Requirements

  1. A strong understanding of Hypermedia Systems.

  2. htmx.

  3. Ruby.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install htmx --trust-policy HighSecurity

To install without security, run:

gem install htmx

You can also add the gem directly to your project:

bundle add htmx

Once the gem is installed, you only need to require it:

require "htmx"

Usage

One of the first tasks, when working with the htmx library, is building htmx specific HTML attributes. This can be accomplished by using the .[] method. Example:

HTMX[target: "closest .task", delete: "/tasks/1"]

# {
#   "data-hx-target" => "closest .task",
#   "data-hx-delete" => "/tasks/1"
# }

Notice the appropriate htmx data-hx* attributes are built which are compatible with the htmx library while not having to manually prefix each one of these attributes with the hx-data- prefix. You can use symbols, strings, or a mix of both as well.

HTML Attribute Prefixes

As shown above, building htmx attributes takes minimal effort but if you’d prefer to only use the HTML hx- prefix, which the htmx library supports, you can customize by using the following:

prefixer = HTMX::Prefixer.new "hx"
prefixer.call(target: "closest .task", delete: "/tasks/1")

# {
#   "hx-target" => "closest .task",
#   "hx-delete" => "/tasks/1"
# }

As you can see, only hx-* keys are produced. By the way, the HTMX::Prefixer is called when using the htmx .[] as shown earlier. This means the following are equivalent:

HTMX[delete: "/tasks/1"]
HTMX::Prefixer.new.call delete: "/tasks/1"
HTMX::Prefixer.new("data-hx").call delete: "/tasks/1"

All three of the above will produce the same output which means you’ll most likely want to use the .[] method since it has the shortest syntax. If you attempt to use an unsupported prefix, you’ll get an error:

HTMX::Prefixer.new :bogus
# Invalid prefix: :bogus. Use: "hx" or "data-hx". (HTMX::Error)

Some htmx attributes use dashes. For those situations, you can use strings for keys or underscored symbols to produce the correct htmx syntax. Here’s an example using both a string and symbol for keys:

HTMX["swap-oob" => true, push_url: "/demo/123"]

# {
#   "data-hx-swap-oob" => true,
#   "data-hx-push-url" => "/demo/123"
# }

HTTP Headers

When working with HTTP requests/responses, especially HTTP headers, there are a couple of methods that can parse and make the data easier to work with. Here’s how to use them.

Requests

The request object allows you to obtain a Data object to interact with when parsing htmx HTTP request headers. Example:

HTMX.request

# #<data HTMX::Headers::Request:0x00000a10
#   boosted = nil,
#   current_url = nil,
#   history_restore_request = nil,
#   request = nil,
#   request_type = nil,
#   source = nil,
#   target = nil
# >

Notice you get a Data instance where all members have the HX- prefix removed while each value defaults to nil. Even better — and more practical — is you can ask the request object to parse the incoming HTTP headers directly and give you exactly what you need:

HTMX.request "HTTP_HX_BOOSTED" => "true",
             "HTTP_HX_CURRENT_URL" => "/demo",
             "HTTP_HX_HISTORY_RESTORE_REQUEST" => "false",
             "HTTP_HX_REQUEST" => "true",
             "HTTP_HX_REQUEST_TYPE" => "partial",
             "HTTP_HX_SOURCE" => "button",
             "HTTP_HX_TRIGGER" => "demo"

# #<data HTMX::Headers::Request:0x00000a80
#   boosted = "true",
#   current_url = "/demo",
#   history_restore_request = "false",
#   request = "true",
#   request_type = "partial",
#   source = "button",
#   target = nil
# >

As you can see, this method only plucks out the htmx specific headers which may or may not have values. Extra header keys, which are not specific to htmx, are ignored.

As an added convenience, you can use predicate methods for boolean values. Example:

headers = HTMX.request "HTTP_HX_BOOSTED" => "true",
                       "HTTP_HX_CURRENT_URL" => "/demo",
                       "HTTP_HX_HISTORY_RESTORE_REQUEST" => "false",
                       "HTTP_HX_REQUEST" => "true",
                       "HTTP_HX_REQUEST_TYPE" => "partial",
                       "HTTP_HX_SOURCE" => "button",
                       "HTTP_HX_TRIGGER" => "demo"

headers.boosted?                  # true
headers.history_restore_request?  # false
headers.request?                  # true

Due to the HTML.request delegating to the HTMX::Headers::Request, this means you can use the object directly. Specifically, you can obtain the record, key, or header depending on your needs. Example:

HTMX::Headers::Request.for(**headers)                 # Identical to `HTMX.request`.
HTMX::Headers::Request.key_for "HTTP_HX_CURRENT_URL"  # :current_url
HTMX::Headers::Request.header_for :current_url        # "HTTP_HX_CURRENT_URL"

Should you not want to use HTMX::Headers::Request, you can use the request predicate methods instead. Example:

headers = {}

HTMX.request! headers, boosted: true, source: "button"
# {
#   "HTTP_HX_BOOSTED" => true,
#   "HTTP_HX_SOURCE" => "button"
# }

HTMX.request? headers, :source, "button"  # true
HTMX.request? headers, :source, /but/     # true
HTMX.request? headers, :source, "input"   # false
HTMX.request? headers, :source, /input/   # false

💡 HTMX.request! mutates your original headers. Unknown attributes are merged as is.

Responses

The response object allows you to obtain a Data object to interact with when parsing htmx HTTP response headers. Example:

HTMX.response

# #<data HTMX::Headers::Response:0x00001830
#   location = nil,
#   push_url = nil,
#   redirect = nil,
#   refresh = nil,
#   replace_url = nil,
#   reselect = nil,
#   reswap = nil,
#   retarget = nil,
#   trigger = nil
# >

Notice you get a Data instance where all members have the HX- prefix removed while each value defaults to nil. Even better — and more practical — is you can ask the response object to parse the incoming HTTP headers directly and give you exactly what you need:

HTMX.response "HX-Location" => "/",
              "HX-Push-Url" => "/demo",
              "HX-Redirect" => "/demo",
              "HX-Refresh" => "true",
              "HX-Replace-Url" => "/demo",
              "HX-Reselect" => "#demo",
              "HX-Reswap" => "none",
              "HX-Retarget" => ".demo",
              "HX-Trigger" => "demo"

# #<data HTMX::Headers::Response:0x00001850
#   location = "/",
#   push_url = "/demo",
#   redirect = "/demo",
#   refresh = "true",
#   replace_url = "/demo",
#   reselect = "#demo",
#   reswap = "none",
#   retarget = ".demo",
#   trigger = "demo"
# >

As you can see, this method only plucks out the htmx specific headers which may or may not have values. Extra header keys, which are not specific to htmx, are ignored.

There is also a refresh predicate method that’ll answer a boolean for convenience. Example:

headers = HTMX.response "HX-Refresh" => "true"

headers.refresh? # true

Due to HTML.response delegating to the HTMX::Headers::Response, this means you can use the object directly. Specifically, you can obtain the record, key, or header depending on your needs. Example:

HTMX::Headers::Response.for(**headers)         # Identical to `HTMX.response`.
HTMX::Headers::Response.key_for "HX-Location"  # :location
HTMX::Headers::Response.header_for :location   # "HX-Location"

Should you not want to use HTMX::Headers::Response, you can use the response predicate methods instead. Example:

headers = {}

HTMX.response! headers, location: "/", push_url: "/demo"
# {
#   "HX-Location" => "/",
#   "HX-Push-Url" => "/demo"
# }

HTMX.response? headers, :push_url, "/demo"   # true
HTMX.response? headers, :push_url, /emo/     # true
HTMX.response? headers, :push_url, "/about"  # false
HTMX.response? headers, :push_url, /about/   # false

💡 HTMX.response! mutates your original headers. Unknown attributes are merged as is.

Errors

Any/all errors issued by this gem will be an instance of the HTMX::Error class which inherits from StandardError. You can use this classification to catch and deal with these errors in your own implementation as desired.

Development

To contribute, run:

git clone https://github.com/bkuhlmann/htmx
cd htmx
bin/setup

You can also use the IRB console for direct access to all objects:

bin/console

Tests

To test, run:

bin/rake

Credits

About

An augmenter and companion to the HTMX JavaScript library.

Topics

Resources

Stars

19 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages