How to Use Alpine AJAX Integration for Partial HTML Updates in Topcoat

Enable the alpine-ajax feature in your Cargo.toml, load the Alpine AJAX and Alpine.js scripts with defer attributes, then use ajax_request(cx) to detect AJAX requests and return only the fragments requested via ajax_target(cx, "id") or ajax_targets(cx).

The tokio-rs/topcoat framework provides first-class support for Alpine AJAX integration, allowing you to build server-rendered Rust applications with partial HTML updates without writing custom JavaScript. By leveraging the topcoat-alpine-ajax module, you can inspect request headers to return precisely the fragments the client expects, yielding snappy UI updates while maintaining progressive enhancement.

Enabling the Alpine AJAX Feature

To access the integration helpers, enable the feature flag in your Cargo.toml. This exposes the topcoat::alpine_ajax module behind the alpine-ajax feature gate.

[dependencies]
topcoat = { version = "0.5.0", features = ["alpine-ajax"] }

You must also load the client-side scripts in your layout. Alpine AJAX must load before Alpine.js, and both require the defer attribute to ensure they run after the document parses.

view! {
    <head>
        <script defer src="https://cdn.jsdelivr.net/npm/@imacrayon/alpine-ajax@0.12.4/dist/cdn.min.js"></script>
        <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js"></script>
    </head>
}

Detecting Alpine AJAX Requests

When Alpine AJAX initiates a request, it sends the X-Alpine-Request: true header. In crates/topcoat-alpine-ajax/src/request.rs, the ajax_request function checks for this header.

Use this in your layout function to distinguish between full-page renders and partial updates:

use topcoat::alpine_ajax::ajax_request;
use topcoat::context::Cx;
use topcoat::router::layout;
use topcoat::view::view;

#[layout]
async fn root(cx: &Cx, slot: Result<impl IntoResponse, Response>) -> Result<Response, Response> {
    if ajax_request(cx) {
        // Return only the page content for AJAX responses.
        return slot;
    }

    // Full-page render for standard navigation.
    view! {
        <html>
            <head>
                <script defer src="https://cdn.jsdelivr.net/npm/@imacrayon/alpine-ajax@0.12.4/dist/cdn.min.js"></script>
                <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js"></script>
            </head>
            <body>
                <nav>"Navigation"</nav>
                <main>(slot?)</main>
            </body>
        </html>
    }
}

Targeting Specific Fragments

Alpine AJAX sends the X-Alpine-Target header containing space-separated element IDs that need refreshing. Topcoat provides two helpers in request.rs to inspect these targets:

  • ajax_targets(cx) – Returns an iterator over all requested IDs.
  • ajax_target(cx, id) – Returns true if a specific ID is requested.

Use these to conditionally render fragments:

use topcoat::alpine_ajax::{ajax_targets, ajax_target};
use topcoat::context::Cx;

// Check if a specific fragment is requested.
if ajax_target(cx, "comments") {
    // Render only the comments list...
}

// Or iterate over all targets.
for target in ajax_targets(cx) {
    println!("Client requested refresh of: {}", target);
}

Accessing Raw Header Constants

If you need manual header access, crates/topcoat-alpine-ajax/src/header.rs exposes the header names as HeaderName constants:

use topcoat::alpine_ajax::header::{X_ALPINE_REQUEST, X_ALPINE_TARGET};

// Manual header inspection.
if let Some(targets) = cx.request().headers().get(X_ALPINE_TARGET) {
    // Parse targets manually...
}

Complete Partial Update Handler

Combine these helpers to return only the fragments requested. This example from examples/alpine-ajax/src/main.rs demonstrates returning a form fragment when specifically targeted, while always including a global alert that uses Alpine AJAX's x-sync attribute:

use topcoat::{
    Result,
    context::Cx,
    router::{StatusCode, response::{IntoResponse, Response}, route},
    view::view,
    alpine_ajax::ajax_target,
};

#[route(POST "/comments")]
async fn create_comment(cx: &Cx) -> Result<Response, Response> {
    // Process form validation...

    // Render form fragment only when requested.
    let form_fragment = if ajax_target(cx, "comment_form") {
        view! {
            <form id="comment_form" x-target="comment_form comments" method="post" action="/comments">
                <textarea name="body"></textarea>
                <button type="submit">"Post Comment"</button>
            </form>
        }?
    } else {
        view! {}?
    };

    // Always include alert for x-sync elements.
    let alert = view! {
        <div id="alert" x-sync role="status">
            <p>"Comment posted successfully!"</p>
        </div>
    }?;

    (StatusCode::OK, form_fragment, alert).into_response(cx)
}

The x-sync attribute ensures elements refresh automatically when their ID appears in the response, regardless of the explicit x-target list. This is ideal for flash messages living outside the form's target container.

Summary

  • Enable the feature by adding features = ["alpine-ajax"] to your topcoat dependency in Cargo.toml.
  • Load scripts with defer in the order: Alpine AJAX first, then Alpine.js.
  • Detect requests using ajax_request(cx) from crates/topcoat-alpine-ajax/src/request.rs to skip layout rendering for AJAX calls.
  • Filter fragments with ajax_target(cx, "id") or iterate over ajax_targets(cx) to return only requested HTML.
  • Access headers directly via X_ALPINE_REQUEST and X_ALPINE_TARGET constants from crates/topcoat-alpine-ajax/src/header.rs when needed.
  • Use x-sync for global elements like alerts that should update across all requests.

Frequently Asked Questions

What is Alpine AJAX and how does it work with Topcoat?

Alpine AJAX is a plugin for Alpine.js that enables HTML-over-the-wire updates by intercepting link clicks and form submissions. When integrated with Topcoat, the client sends X-Alpine-Request and X-Alpine-Target headers, allowing your Rust handlers to return partial HTML fragments instead of full pages. Topcoat provides helper functions in topcoat::alpine_ajax to detect these requests and extract the target IDs.

How do I check if the current request is an Alpine AJAX request?

Call ajax_request(cx) passing the request context. This function, defined in crates/topcoat-alpine-ajax/src/request.rs, returns true when the X-Alpine-Request header is present. Use this in your layout functions to conditionally return only the slot content versus the full HTML document.

Can I return multiple fragments in a single response?

Yes. Combine multiple fragment strings in your response tuple. Alpine AJAX expects all requested fragments (and any x-sync elements) in the response body. The client will automatically distribute the HTML to the corresponding element IDs. Use ajax_targets(cx) to iterate over all requested IDs when deciding which fragments to generate.

What is the difference between ajax_targets and ajax_target?

ajax_targets(cx) returns an iterator over all IDs in the X-Alpine-Target header, useful when you need to check multiple possible fragments. ajax_target(cx, "specific-id") returns a boolean indicating whether a particular ID was requested, which is more ergonomic when you only care about one specific fragment. Both functions are exported from topcoat::alpine_ajax and defined in crates/topcoat-alpine-ajax/src/request.rs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →