Heimdall Docs

JavaScript Interop

Heimdall can return a response directive that asks the browser runtime to call an existing JavaScript function.

This is intentionally a void interop surface. It is for browser-side effects such as toasts, focus, analytics, and local integration hooks, not for returning data back into the server response.

1. What It Solves

Most Heimdall interactions should be expressed as HTML returned by the server. JavaScript interop is for the small number of cases where the browser needs to do something that is not naturally an HTML swap.

Good fits:
- show a client toast
- focus an element
- call an analytics hook
- close a third-party widget
- notify a small browser-side integration

Not a fit:
- fetching data back into the action
- running arbitrary script source from a response
- replacing normal HTML rendering

2. Basic Helper

Use JsInvokeVoid from a content action to include a JavaScript invocation directive in the returned HTML. The helper writes an instruction element that the runtime consumes and removes.

[ContentInvocation("orders.save")]
public static IHtmlContent Save()
{
    return Html.Fragment(
        SavedBanner.Render(),
        HeimdallHtml.JsInvokeVoid(
            "window.App.toast.success",
            "Saved"));
}

3. Fluent Fragment Helpers

When a response is built with a FluentHtml fragment, the fluent Heimdall helpers can add the same directive.

return FluentHtml.Fragment(fragment =>
{
    fragment.Add(SavedBanner.Render());

    fragment.Heimdall()
        .JsInvokeVoid("window.App.toast.success", "Saved");
});

4. Timing

JavaScript invocation runs after swaps by default. Use the before helper when the JavaScript must observe the old DOM before the response update is applied.

// Default: after response swaps
HeimdallHtml.JsInvokeVoid(
    "window.App.afterSave",
    orderId);

// Explicit after
HeimdallHtml.JsInvokeVoidAfter(
    "window.App.afterSave",
    orderId);

// Before the main swap
HeimdallHtml.JsInvokeVoidBefore(
    "window.App.captureBeforeSwap",
    "#orders");

5. Explicit Function Paths

Function names must be explicit dotted paths rooted at window, globalThis, or document. Heimdall does not guess whether a bare name belongs to the global object, a module, or another scope.

Valid:
window.App.toast.success
globalThis.App.analytics.track
document.body.focus

Rejected:
App.toast.success
window.App["toast"]
import("/toast.js").then(...)

6. Imported Functions

If your application already imports a function through a normal browser module, expose the callable integration point explicitly before Heimdall needs to call it.

import { showToast } from "/js/toasts.js";

window.App = window.App || {};
window.App.toast = {
  success(message) {
    showToast({ kind: "success", message });
  }
};

// Server response:
HeimdallHtml.JsInvokeVoid(
    "window.App.toast.success",
    "Saved");

7. Arguments

Arguments are serialized as a JSON array and passed to the target function in order. Prefer small values that describe the side effect instead of large application state.

HeimdallHtml.JsInvokeVoid(
    "window.App.order.saved",
    order.Id,
    new
    {
        customer = order.CustomerName,
        total = order.Total
    });

// Browser receives:
window.App.order.saved(42, {
  customer: "Ada",
  total: 129.95
});

8. Return Values

Return values are ignored. If the JavaScript function returns a value or a Promise, Heimdall does not feed that value back into the action or response pipeline.

window.App.toast.success = message => {
  showToast(message);
  return "ignored";
};

window.App.saveAnalytics = async id => {
  await sendAnalytics(id);
  return { also: "ignored" };
};

9. Error Events

If the function path cannot be resolved or the call fails, the runtime emits heimdall:javascript-error. Turn on debug for console output while developing.

Heimdall.config.debug = true;

document.addEventListener(
  "heimdall:javascript-error",
  event => {
    console.warn(event.detail);
  });

10. Redirect, Abort, and SSE

JavaScript directives participate in the same response processing model as other Heimdall directives. Redirect wins and stops later work. Abort can suppress the main swap while still allowing directive processing. SSE payloads can also carry JavaScript directives.

// In an action response
return Html.Fragment(
    HeimdallHtml.Abort("validation-failed"),
    HeimdallHtml.JsInvokeVoid(
        "window.App.form.shake",
        "#checkout-form"));

// In an SSE payload
fragment.Heimdall()
    .JsInvokeVoidAfter(
        "window.App.notifications.pulse",
        "orders");

11. Why There Is No Registration Step

Heimdall does not require server-side registration of JavaScript functions. The server names an explicit browser function path, and the browser resolves that path at runtime. That keeps ownership where it belongs: C# owns the response directive, JavaScript owns the JavaScript function.

Server owns:
HeimdallHtml.JsInvokeVoid("window.App.toast.success", "Saved")

Browser owns:
window.App.toast.success = message => { ... }

12. Safety Model

Heimdall does not evaluate JavaScript source from responses. The directive only names an existing function and passes JSON arguments to it.

Allowed:
call this existing function path with these JSON args

Not allowed:
eval this source code
import this module dynamically
execute bracket expressions
infer a bare global name