Behavior
- Click on a link → GET AJAX request to
href - Ctrl/Cmd+Click → Opens in a new tab (normal)
- Middle click → Opens in a new tab (normal)
- Links with
#→ skipped - After a successful response → URL is added to browser history (pushState)
ln-ajax is applied to containers, links, or forms via data-ln-ajax. It intercepts same-origin anchor clicks and form submissions via e.preventDefault() and issues asynchronous requests with X-Requested-With: XMLHttpRequest headers. When the server responds with targeted HTML fragments (such as { content: { "#target": "<html>" } }), ln-ajax replaces those DOM regions in-place and updates the browser history via history.pushState(), enabling SPA-speed page transitions while preserving server-rendered markup.
In ln-ashlar, this navigation and swapping primitive avoids client-side template bloat by letting the server remain the authoritative source for markup.
An AJAX primitive has no internal routes, layout schemas, or database caches. It expects the server to act as the single source of truth for pages, returning structured JSON payloads containing page titles, DOM target selectors, and new HTML chunks to inject. The server-rendered HTML fragment is the final contract.
data-ln-ajax attribute. Clicking a link or submitting a form initiates the asynchronous lifecycle.
.ln-ajax--loading to the container, and dispatches a bubbled ln-ajax:before-start event.For the full guide see the ln-ajax README and the architecture reference.
| Attribute | On element | Description |
|---|---|---|
data-ln-ajax | container, <a>, <form> | Activates AJAX handling |
data-ln-ajax="false" | <a>, <form> | Exclude a specific element from AJAX |
href# → skipped<div data-ln-ajax>
<a href="/users">Users</a>
<a href="/settings">Settings</a>
<!-- Exclude a specific link -->
<a href="/login" data-ln-ajax="false">Log in</a>
</div>
<form data-ln-ajax method="POST" action="/api/users">
<input type="text" name="name" placeholder="Name">
<input type="email" name="email" placeholder="Email">
<button type="submit">Save</button>
</form>
The server returns JSON with the following structure:
{
"title": "New page title",
"content": {
"main-content": "<h1>Content</h1>",
"sidebar-nav": "<ul><li>...</li></ul>"
},
"message": {
"type": "success|error|warn|info",
"title": "Title",
"body": "Message",
"data": {}
}
}
| Field | Description |
|---|---|
title | <title> attribute of the page |
content | Object: selector → HTML for replacement |
message | Message — available in the ln-ajax:success event |
The element receives the class .ln-ajax--loading. Use it for visual feedback.
a.ln-ajax--loading {
opacity: 0.6;
pointer-events: none;
}
button.ln-ajax--loading {
cursor: wait;
}
The component automatically reads the CSRF token from <meta name="csrf-token"> and sends it in the header:
<head>
<meta name="csrf-token" content="...">
</head>
// ln-ajax reads it and sends it as:
// X-CSRF-Token: ...
ln-ajax has no public API — it is activated automatically via data attributes.
// Manual initialization of a new DOM element
window.lnAjax(document.getElementById('new-container'));
| Event | When | detail |
|---|---|---|
ln-ajax:before-start | Before everything (cancelable — can cancel request) | { method, url } |
ln-ajax:start | After adding spinner, before fetch | { method, url } |
ln-ajax:success | After successful response | { method, url, data } |
ln-ajax:error | After fetch error | { method, url, status, data, error } |
ln-ajax:complete | After completion (success or error) | { method, url } |
ln-ajax:aborted | A newer GET superseded this in-flight GET (instead of success/error/complete) | { method, url } |
Events are dispatched on the element that initiated the request (<a> or <form>) and bubble up.
ln-ajax stays isolated and emits ln-ajax:success and ln-ajax:error. The Layer 2 coordinator (ln-ui-coordinator) intercepts these bubbling events, inspects the response data.message envelope, and dispatches ln-toast:enqueue on window.
// Cancel a request conditionally (ln-ajax:before-start is cancelable)
document.addEventListener('ln-ajax:before-start', function(e) {
if (!userIsAuthenticated()) {
e.preventDefault(); // request is cancelled, no spinner
}
});
// Global loading indicator
document.addEventListener('ln-ajax:start', function(e) {
document.body.classList.add('loading');
});
document.addEventListener('ln-ajax:complete', function(e) {
document.body.classList.remove('loading');
});
// Refresh components after DOM change
document.addEventListener('ln-ajax:success', function(e) {
window.lnSelect && window.lnSelect.reinit();
});
// Error handler
document.addEventListener('ln-ajax:error', function(e) {
console.error('AJAX failed:', e.detail.url, e.detail.error);
});