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 1 if the incoming request was triggered by htmx (checks HX-Request header), otherwise returns 0.

is_boosted()

Returns 1 when HX-Boosted is true.

is_history_restore()

Returns 1 when HX-History-Restore-Request is true.

current_url()

Returns the browser URL supplied in HX-Current-URL, or undef.

prompt()

Returns the response to hx-prompt, including an intentionally empty string, or undef when HX-Prompt was not sent.

target()

Returns the string ID or CSS selector of the target element sent by the browser via the HX-Target header, if present.

trigger_id()

Returns the ID of the specific frontend DOM element that triggered the request via the HX-Trigger request header.

trigger_name()

Returns the name of the triggering element from HX-Trigger-Name, or undef.

trigger_event()

Inspects the incoming HX-Trigger request 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 undef if 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-Retarget HTTP header.

res_reswap( $strategy )

Overrides the layout swap strategy (e.g., 'outerHTML', 'innerHTML', 'none'). Maps to the outbound HX-Reswap HTTP 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-Location form.

res_push_url( $url_or_false )

Sets HX-Push-Url. Pass a URL, or the literal string false to prevent a history entry.

res_replace_url( $url_or_false )

Sets HX-Replace-Url. Pass a URL, or the literal string false to prevent URL replacement.

res_redirect( $url )

Requests a full client-side redirect using HX-Redirect.

res_refresh( [$boolean] )

Sets HX-Refresh to true by default. Passing a false Perl value sets it to false.

res_trigger_name( $element_name )

Sets the HX-Trigger-Name outbound 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-Trigger HTTP header.

res_trigger_after_settle( $event_name [, $params ] )

Like res_trigger, but emits HX-Trigger-After-Settle.

res_trigger_after_swap( $event_name [, $params ] )

Like res_trigger, but emits HX-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 _in if headers should be read lazily from a framework object instead of being normalized eagerly in new. 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, and HTTP_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

Uniform

Uniform::HTMX::PSGI

Uniform::HTMX::Mojolicious

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