Zjax
Zjax is a lightweight yet powerful JavaScript library (5K gzipped) that brings modern SPA-like interactivity to your web pages with minimal effort. By simply adding z-swap
and z-action
attributes to your HTML elements, you can dynamically update parts of a web page or bind client-side JavaScript actions directly to the DOM — all without writing any verbose JavaScript code.
Inspired by HTMX, Hotwire, and AlpineJS and compatible with any SSR backend like Rails, Laravel, Django, Astro – or even Wordpress, Zjax seamlessly integrates into your workflow.
Why not just use HTMX or Hotwire?
HTMX and friends broke new ground implementing the idea of declarative AJAX to be sprinkled into the DOM. Zjax implements the same functionality with simpler syntax and also facilitates client-side JavaScript without the need for an additional JS library like Alpine or Stimulus.
Getting started
Just include the Zjax CDN link in your document head.
<head>
<script src="https://unpkg.com/zjax@3.0.4"></script>
...
</head>
You can now use Zjax attributes anywhere in your project.
z-swap
The main workhorse of Zjax is the z-swap
attribute which can be added to any HTML tag to specify the elements we want to swap.
You can try this right now with a plain HTML file.
index.html
<html>
<head>
<script src="https://unpkg.com/zjax"></script>
</head>
<body>
<h1>This is Zjax</h1>
<a href="https://httpbin.org/html" z-swap="@click.prevent p">
Fetch Moby Dick
</a>
<p>This will be replaced by Zjax.</p>
</body>
</html>
Adding the z-swap
attribute hijacks this link so that its default behavior is replaced with an AJAX request. The first p
element is then plucked from response HTML and used to replace our local p
tag without affecting any other parts of the page.
In this example, we specified only the trigger-event @click
with the prevent
modifier to prevent the browser from actually navigating to the new page. Then we specified that the element to be swapped is the p
-tag. The other specifiers are inferred from context. By default, the HTTP method will be GET
, and the endpoint URL for an a
tag can be inferred from href
value. But these can also be defined explicitly. For the example above, this would be the same:
<a href="" z-swap="@click.prevent GET https://httpbin.org/html p">
Notice that we've omitted the href
value as it would be ignored anyway since the explicitly specified endpoint URL takes precedence.
Format of z-swap
value
z-swap="@<event> [method] [endpoint] [swap-elements]"
Only the @<event>
specifier is required. The other three specifiers are optional as long as they can be inferred from context. Each specifier is separated by a space. Remember that the trigger must always be prefixed with @
and that a valid endpoint must always start with http://
, https://
, /
, ./
, or it can be a single dot, .
.
Specifying other event events
Try changing the trigger event to @mouseover
– and since there is no default behavior for a mouseover event on an anchor tag, we no longer need to use the prevent
modifier to prevent navigation.
<a href="https://httpbin.org/html" z-swap="@mouseover p">
Fetch Moby Dick
</a>
Any standard DOM event as well as some special ones used by Zjax and any custom events you have defined globally can be specified prefixed with an @
-sign like @submit
, @blur
, @input
, @dblclick
, @my-custom-event
, etc.
Note that @submit
can be used only on a form
element.
The special @mount
trigger
This event will fire when the element is loaded into the DOM. This works for initial page load as well as when elements are loaded into the DOM via a z-swap.
Heads up! Be careful not to create an infinite loop by swapping an element which has a
@mount
trigger into itself.
The special @action
trigger
When using a z-swap
in conjunction with a z-action
on the same element, the trigger can be set to @action
which will be triggered when the z-action
function returns any "truthy" value. This works for both asynchronous and synchronous functions.
Specifying the Endpoint
The example above infers the endpoint from the a
-tag's href
value. But for a button
, there is no href
attribute – so we'll need to specify that too as part of the z-swap
value. The default HTTP method GET
is fine here so we'll omit it.
<button z-swap="@dblclick https://httpbin.org/html p">
Fetch Moby Dick
</button>
The endpoint specifier can be any valid URL including local absolute or relative paths as long as it starts with http://
or https://
, or starts with /
, or ./
, or is a single dot .
. Note that the endpoint must start with one those options or it will not be recognized as a endpoint.
Specifying the HTTP Method
The example above will use the GET
method which is the default when using z-swap
on any element except a <form>
element with a method
attribute set to method="post"
. The HTTP methods GET
, POST
, PUT
, PATCH
, or DELETE
are supported.
<button z-swap="@click DELETE /books/123 #book-form">
Click me
</a>
Specifying the SWAP Element
The swap element is specified with CSS selector syntax like p
, #cart
, or nav>a
. If only one element is specified, it will be used to identify both the response element and the target element to be replaced. Specifying multiple elements to swap at once as well as specifying separate response and target element selectors are also supported. The default swap-type used to replace the entire element can also be changed.
Swapping Multiple Elements
We aren't limited to swapping just one element. Multiple elements can be swapped at the same time separated by commas.
<button z-swap="@click /books/123 #book-form,#cart">
Click me
</a>
In the example above, both the #book-form
and #cart-total
elements will be swapped out and replaced with matching elements found in the response.
Swapping response->target elements
Sometimes the selector for the target element isn't the same as the response that you want to swap in. Use the ->
operator to specify the response and target elements separately.
<button z-swap="@click /books/123 #book-form, #updated-cart->#cart">
Click me
</a>
Use a *
character to specify the entire page content.
<button z-swap="@click /books/123 *->#book-detail">
Click me
</a>
In the above example, presumably the /books/123
route returns a partial containing only the elements we need.
The wildcard *
element
If a *
is used by itself as the response swap element specifier, Zjax will use entire response. This is most useful when the response is known to be a partial containing only the element or elements needed for the swap.
For completeness, *
can also be used for the target element specifier although it probably isn't all that useful since this effectively just replaces the body element contents.
Specifying the Swap-Type
The default swap-type is outer
which replaces the element in its entirety. Alternatively, you may want to replace only the inner content of the element, or maybe insert the response element after the target. The swap-type can be appended to the target element using the pipe |
character. Note that the swap-type only affects the target element.
<button z-swap="@click /books/123 #cart-total|after">
Click me
</a>
Swap types available include:
outer
- Morph the entire element (default)inner
- Morph only inner contentbefore
- Insert before this elementprepend
- Insert before all other inner contentafter
- Insert after this elementappend
- Insert after all other inner contentdelete
- Ignore returned value and delete this elementnone
- Do nothing (typically used with dynamic values)
Specifying the Response-Type
The default response-type is outer
which means that the element found in the response will be used in its entirety. To use only the content found within the response element, you can specify the inner
response type like this:
<button z-swap="@click /books/123 #books-table|inner->#books-rows|inner">
Click me
</a>
In this example, only the inner contents of the response element will be used to replace only the inner contents of the target element.
Response-Types available include:
outer
- Use the entire response element (default)inner
- Use only inner content of the response element
z-action
The z-action
attribute is used to bind a Javascript method to an element's event listener with syntax similar to z-swap
. So use z-swap
to interact with a remote server and use z-action
to handle client-side only Javascript actions where no round trip to the server is needed (like closing a modal window).
In this example, a dblclick
event listener is added to the event which will call the doSomething()
action.
<div z-action="@dblclick doSomething">
Do it now!
</div>
In order for this action to work, we need to define it somewhere in our project as Zjax Action like this:
<script>
zjax.actions = {
doSomething() {
alert("I did something!");
}
}
</script>
Defining Actions
Actions can be registered directly like this:
zjax.actions = {
openModal() {
...
},
closeModal() {
...
},
async handleFileUploadDrop() {
...
}
}
...or as a namespaced property like this:
zjax.actions.products = {
addToCart() {
...
},
removeFromCart() {
...
}
}
For the namespaced example above, the z-action
would be prefixed with products
like this:
<button z-action="@click products.addToCart">
Add To Cart
</button>
It's possible to organize your code any way you like. For a complex application for example you may want to create an actions/
directory with separate namespaced files for products.js
, account.js
, and so on.
How Actions work
Naturally, a z-action
handler method may contain any valid Javascript. Vanilla JS is very powerful these days allowing for full access to DOM manipulation without the need for any JQuery-like library. But Zjax does actually give us a very handy tool anyway just to make life a bit easier.
Adding event listeners with z-action
Manually adding event listeners from a JS script somewhere else in your project can be tedious and difficult to manage. Event listeners will also need to be removed at some point or they can start stacking up and eventually cause memory leak issues.
By using a z-action
attribute, this not only sets up the listener and associates it with a function, but also removes it automatically when the element leaves the DOM.
Heads up!
Watch out for this quirk of HTTP. When a
<script>
tag is added to the DOM for example by az-action
, it will be ignored by the browser. This means that you can't declare Zjax Actions within a partial, for example. Of course you can usez-action
attributes in your partials and these will be parsed just fine. But setting the actualzjax.action
value to define an action cannot be called within a<script>
tag contained in a swap response because your browser won't execute anything found within script tags in a loaded partial.
The $
Action Helper object
Action functions can receive a $
argument.
doSomething($) {
... // Now can you acccess the $ object
}
This object is called the Action Helper and it provides a few handy properties and methods.
$()
returns the element which triggered this action when no selector is provided.$(<selector>)
is a shortcut fordocument.querySelector(<selector>)
.$.all(<selector>)
is a shortcut fordocument.querySelectorAll(<selector>)
.$.event
returns theevent
object which triggered this action.$.redirect(<url>)
is a shortcut forwindow.location=<url>
Applying inline functions directly to z-action
For short snippets of functionality like toggling a class when a button is clicked, Zjax supports defining a function directly as the Zjax value like this:
<button z-action="@click $('#menu').classList.add('open')">
Open menu
</button>
Zjax is smart enough to recognize when the value looks like an action name and will try to find that method in registered actions. If the value doesn't look like an action name – or even if it does but no such method has been defined on zjax.actions
, then the value will be treated as a custom inline function.
Using a Zjax Action as a z-swap
trigger
Sometimes it makes sense to trigger a z-swap
action only once a z-action
has completed successfully. For example, a z-action
could be used to await confirmation before executing a dangerous action.
Zjax makes this very simple.
- Specify
@action
as the Trigger event for thisz-swap
. - Return
true
from the Zjax Action function to trigger thez-swap
.
Note that this works for synchronous and asynchronous functions alike.
<button
z-swap="@action DELETE /books/{id}"
z-action="@click return confirm('Are you sure?')"
>
Delete
</button>
Notice in this example that the inline action function returns the boolean value of the confirm function – but you could also return any truthy or un-truthy value either from an inline action or a named action registered on the global zjax object.
Multiple statements
A "statement" is the combination of a trigger event and the swap or action handler text. But what if we want to listen for more than one trigger, each with it's own effect? For example, we might want to create a z-action
which does one thing on mouseover
and another on click
. We can do that by simply separating statements with commas.
Here's what that looks like:
z-action="@mouseover handleMouseover, @click handleClick"
Trigger event modifiers
Trigger events for both z-swap
and z-action
can have their behavior modified with one or more modifiers. To listen for a click event only outside the element (helpful for closing a menu when clicked outside of the menu, for example), use @click.outside
. To listen for the escape key keydown
event on the entire window (as opposed to only when the element itself is focused), use @keydown.window.escape
. To debounce input events every 500 ms, use @input.debounce.500ms
.
Global trigger modifiers
document
- Attach event listener to the document instead of this elementwindow
- Attach event listener to the window instead of this elementprevent
- Callsevent.preventDefault()
for this trigger eventstop
- Callsevent.stopPropagation()
for this trigger eventonce
- Trigger event will listen only for the first eventdelay
- Delay for n-milliseconds or n-seconds Example:@click.delay.500ms
or@click.delay.1s
debounce
- Multiple trigger events within the specified time will wait until specified time has passed before firing again Example:@click.debounce.200ms
Keyboard trigger event modifiers
Note that most often, we'll want to listen for keyboard events on the window
as opposed to the element itself (see examples below).
- A single key name like
n
,k
,?
,escape
,uparrow
Example:@keydown.window.n
any
- Adds a listener for any key Example:@keydown.window.any
meta
- Require meta key to also be held down simultaneously Example:@keydown.window.meta.p
alt
- Require alt key to also be held down simultaneously Example:@keydown.window.alt.p
ctrl
- Require ctrl key to also be held down simultaneously Example:@keydown.window.ctrl.p
shift
- Require shift key to also be held down simultaneously Example:@keydown.window.shift.p
Some other handy named keys
enter
escape
tab
backspace
delete
insert
home
end
pageup
pagedown
arrowup
arrowdown
arrowleft
arrowright
space
comma
period
semicolon
singlequote
doublequote
backtick
slash
backslash
bracketleft
bracketright
curlybracketleft
curlybracketright
minus
equal
plus
underscore
ampersand
asterisk
at
dollar
percent
caret
exclamation
tilde
pipe
colon
question
underscore
lessthan
greaterthan
hash
Mouse trigger modifiers
Mouse events can also make use of these trigger modifiers:
meta
- Require meta key to also be held down simultaneously Example:@click.meta
alt
- Require alt key to also be held down simultaneously Example:@click.alt
ctrl
- Require ctrl key to also be held down simultaneously Example:@click.ctrl
shift
- Require shift key to also be held down simultaneously Example:@click.shift
Multiple triggers
Triggers may be combined for cases where multiple events should trigger the same swap or action. For example, to close a modal window on both escape key and click outside triggers:
z-action="@[keydown.window.escape,click.outside] closeModal"
Notice that there is only one @
-sign followed by an array-like syntax. We use comma-separated triggers (with modifiers) enclosed in square brackets.
Handling global z-swap
error responses
When a z-swap fails for example because the endpoint is not found (404 response), or there was a server side error (500 reponse), or because of some other response error code, Zjax will call a callback function that you can define on the zjax.errors
object like this:
// Remember: zjax.errors is _only_ for z-swap when response.ok === false
zjax.errors = {
404() {
alert("This swap failed because the endpoint was not found")
}
}
Under the hood, Zjax uses fetch
just as you might do when manually building your own AJAX calls. When the response
object returned by fetch
contains a status
property with a code (like 404
, or 500
, or 502
, etc), zjax will check for a matching function defined on zjax.errors
. If a matching function is found, it will be executed.
Using $
within error handlers
Defining z-swap
error handler functions is similar to defining z-action
functions in that you can run any valid JavaScript here. The same handy $
helper is also available and works exactly the same as it does within zjax.actions
functions with one additional property: response
. This is the complete response
object returned by fetch
and available for you to use in your handlers to check the statusText
or any other property you might want to access.
zjax.errors = {
404($) {
alert($.response.statusText); // "Not found"
$.redirect('/no-such-page');
}
}
The catchAll
error handler
In addition to handling explicit error status codes, you can define a catchAll
handler which will fire for any response with an ok
property of false unless there is an explicity handler for this given status code.
zjax.errors = {
// This will always fire for a status 403 response
403() {
console.log("You aren't allowed here.");
},
// This will fire for all other status.ok === false responses
catchAll($) {
console.log(`Error: {$.response.statusText}`);
}
}
The zjax
global object
When Zjax is loaded, it creates the global zjax
object which is available everywhere in your app. This means you can organize your action files however you like. You can stick a script tag in your document head, add a separate file like zjax-actions.js
(or whatever), or if your application is complex enough, maybe make a directory called actions/
with separate files for different sections of your application.
There are also a few configuration options:
zjax.debug
- Enables verbose debug logs to the console (default:false
)zjax.transitions
- Enables view transition API for browsers that support it (default:true
)zjax.parse()
- Parse the DOM for z-swaps and z-action tags. This normally happens when the DOM initially loads and also whenturbo:load
events are emitted for use with modern Rails applications using Hotwire. This function can be used to parse the DOM on other events.