NAME
Uniform::HTMX - Extensible, framework-agnostic base layer for htmx communication
SYNOPSIS
This is an abstract base module. It is not used directly for a request — instead, build a small integration for your framework with driver():
my $htmx_driver = Uniform::HTMX->driver(
extract => sub {
my ($scope) = @_; # whatever your framework hands you
return %{ $scope->{headers} }; # raw header name/value pairs
},
apply => sub {
my ($self, $scope, $out_headers) = @_;
$scope->{res}->header($_ => $out_headers->{$_})
for keys %$out_headers;
},
);
# Per request:
my $htmx = $htmx_driver->($scope);
$htmx->is_htmx;
$htmx->res_trigger('saved');
$htmx->apply;
driver() takes two plain subs — extract turns your framework's request into a flat header hash, apply takes the accumulated outbound headers and writes them back however your framework expects. No package declaration, use parent, or constructor is required; every request/response method documented below works immediately once extract and apply are in place.
If you're publishing a reusable, distributable integration (e.g. Uniform::HTMX::PSGI), subclassing directly is the better fit and gives you a normal, discoverable CPAN namespace:
# Inside a subclass (e.g. Uniform::HTMX::PAGI)
package Uniform::HTMX::PAGI;
use parent 'Uniform::HTMX';
sub new {
my ($class, $scope) = @_;
my %raw_headers = ...; # Framework specific extraction
# SUPER::new() normalizes %raw_headers for you and stores
# everything else you pass it (here, the framework's scope object).
my $self = $class->SUPER::new(in => \%raw_headers, _ctx => $scope);
return $self;
}
sub apply {
my ($self) = @_;
# Framework specific response injection using $self->_out
}
In fact, driver() is implemented in terms of exactly this pattern — it generates an anonymous subclass on your behalf and wires your two subs in as new and apply, so the two approaches are fully interchangeable and can even be mixed: call YourExistingSubclass->driver(...) to get the closure shortcut while still inheriting any hooks YourExistingSubclass overrides.
DESCRIPTION
Uniform::HTMX provides a strict, unified interface for interacting with the htmx client-side library. By decoupling the htmx spec protocol from core web frameworks, it prevents backend platform locking and standardizes frontend interactions.
METHODS
driver( extract => \&extract, apply => \&apply )
Class method. Returns a coderef that builds a ready-to-use Uniform::HTMX object per request, without requiring a named subclass. See "SYNOPSIS" for a full example.
extract is called as extract->($scope), where $scope is whatever value you pass to the returned coderef (a PSGI $env, a Mojolicious controller, etc.). It must return a flat list of header name/value pairs, which is normalized the same way _normalize_headers normalizes any other input.
apply is called as apply->($self, $scope, $out_headers, @extra), where $out_headers is the hashref accumulated by res_* method calls (i.e. what _out returns) and @extra is whatever extra arguments you pass to $self->apply(...) at call time — useful for passing a status code and body, as in the PSGI example above.
Calling driver() on a subclass (rather than Uniform::HTMX itself) inherits that subclass's hook overrides (_encode_json, _in, etc.), so the two extension mechanisms compose rather than compete.
Request Inspection
- is_htmx()
-
Returns
1if the incoming request was triggered by htmx (checksHX-Requestheader), otherwise returns0. - is_boosted()
-
Returns
1whenHX-Boostedis true. - is_history_restore()
-
Returns
1whenHX-History-Restore-Requestis true. - current_url()
-
Returns the browser URL supplied in
HX-Current-URL, orundef. - prompt()
-
Returns the response to
hx-prompt, including an intentionally empty string, orundefwhenHX-Promptwas not sent. - target()
-
Returns the string ID or CSS selector of the target element sent by the browser via the
HX-Targetheader, if present. - trigger_id()
-
Returns the ID of the specific frontend DOM element that triggered the request via the
HX-Triggerrequest header. - trigger_name()
-
Returns the name of the triggering element from
HX-Trigger-Name, orundef. - trigger_event()
-
Inspects the incoming
HX-Triggerrequest header and evaluates its contents.Under the htmx specification, if a request is launched due to a client-side JavaScript event, the browser may send a JSON string containing the event name and its parameters instead of a simple element ID string.
This method checks the structural layout of the payload. If it detects a JSON string, it automatically decodes it and returns a native Perl data structure (hash reference or array reference). If it is a normal text string, it returns the raw scalar ID unchanged.
Returns
undefif the header was not sent.
Response Manipulation
All modification methods return $self to support fluent method chaining.
- res_retarget( $css_selector )
-
Overrides the client-side element target target for the incoming HTML swap. Maps to the outbound
HX-RetargetHTTP header. - res_reswap( $strategy )
-
Overrides the layout swap strategy (e.g.,
'outerHTML','innerHTML','none'). Maps to the outboundHX-ReswapHTTP header. - res_reselect( $css_selector )
-
Selects the portion of the response to swap via
HX-Reselect. - res_location( $url_or_hashref )
-
Performs an htmx client-side navigation without a full reload. A scalar sets a URL; a hash reference is JSON encoded for the extended
HX-Locationform. - res_push_url( $url_or_false )
-
Sets
HX-Push-Url. Pass a URL, or the literal stringfalseto prevent a history entry. - res_replace_url( $url_or_false )
-
Sets
HX-Replace-Url. Pass a URL, or the literal stringfalseto prevent URL replacement. - res_redirect( $url )
-
Requests a full client-side redirect using
HX-Redirect. - res_refresh( [$boolean] )
-
Sets
HX-Refreshtotrueby default. Passing a false Perl value sets it tofalse. - res_trigger_name( $element_name )
-
Sets the
HX-Trigger-Nameoutbound response header to specify the name of the target element to trigger client-side, if overriding standard target patterns. - res_trigger( $event_name [, $params_hashref_or_arrayref ] )
-
Instructs htmx to launch a custom client-side JavaScript event upon receiving the response fragment. If params are provided, they will be automatically serialized to valid JSON format as required by the htmx specification. Maps to the outbound
HX-TriggerHTTP header. - res_trigger_after_settle( $event_name [, $params ] )
-
Like
res_trigger, but emitsHX-Trigger-After-Settle. - res_trigger_after_swap( $event_name [, $params ] )
-
Like
res_trigger, but emitsHX-Trigger-After-Swap.
EXTENDING UNIFORM::HTMX
To author a brand-new framework bridge distribution, construct your module namespace under the Uniform::HTMX::* hierarchy and declare Uniform::HTMX as your parent.
Every request-inspection and response-manipulation method above is implemented in terms of a small set of hooks. Override a hook and every public method that depends on it changes behavior automatically — you should never need to reimplement is_htmx, res_trigger, etc. yourself.
Step 1: Extract your framework's headers
Your driver's only required job is turning framework-specific request data into a plain hash of header-name/value pairs, and handing it to the constructor.
Step 2: Call SUPER::new
sub new {
my ($class, $scope) = @_;
my %raw_headers = ...; # your framework-specific extraction
return $class->SUPER::new(in => \%raw_headers, _ctx => $scope);
}
SUPER::new(%args) blesses the object, runs %args's in value through _normalize_headers, and stores every other key you pass verbatim (so you can stash a framework context, request object, etc. right on $self). Using SUPER::new is optional — if your driver needs a completely different construction shape, just bless your own hashref with in and out keys instead.
Step 3: Implement apply()
Uniform::HTMX never writes to the network itself. Your driver's apply() method (called whatever makes sense for your framework) should read the accumulated outbound headers via $self->_out and inject them using your framework's response API.
Hooks you may override
_in()/_out()-
Return the hashrefs backing inbound and outbound headers, respectively. Override
_inif headers should be read lazily from a framework object instead of being normalized eagerly innew. Both default to auto-vivifying hashrefs on$self. _encode_json( $data )/_decode_json( $json )-
Called wherever this module serializes or parses JSON (
res_trigger,res_location,trigger_event, etc.). Override to swap the JSON backend, force canonical key ordering, or pretty-print for debugging. _normalize_headers( \%hash )-
Accepts normal HTTP header names and common CGI/PSGI environment spellings such as
HX-Target,hx_target, andHTTP_HX_TARGET. Names are matched case-insensitively. Malformed names and reference-valued fields are ignored; for array-valued duplicate fields, the last defined scalar is used. If both direct and environment forms are present, the direct HTTP spelling takes precedence. Override only if your framework needs a fundamentally different normalization scheme.
Methods you should leave alone
_request_header, _true_header, _response_header, and _event_header are internal plumbing shared by every public method. _response_header in particular enforces the validation that rejects references and CR/LF characters, preventing invalid headers and response-splitting vulnerabilities — bypassing it to write directly to _out reintroduces that risk. Compose new response methods by calling _response_header or _event_header, not by writing to _out directly.
SEE ALSO
AUTHOR
Joshua S. Day <HAX@cpan.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by Joshua S. Day.
This is free software, licensed under:
The MIT (X11) License