Heimdall Docs

HTML Markup & Attributes

Heimdall is HTML-first. Every interaction helper eventually becomes ordinary markup that the browser runtime reads from the DOM.

Use this page when you want the raw attributes, the C# helper that emits them, and the minimum complete markup needed to make an interaction work.

1. The One Rule

If Heimdall behavior is confusing, inspect the rendered HTML. The runtime does not need a component protocol or JSON view model. It needs attributes on DOM elements.

C# helper
-> rendered HTML attribute
-> browser runtime reads the attribute
-> server action returns HTML
-> runtime swaps that HTML into the DOM

2. Raw Markup Is Always Valid

You can write Heimdall attributes directly in Razor, MVC partials, static HTML, templates, or any server-rendered markup. The fluent helpers are convenience APIs, not a second runtime.

<button
  type="button"
  heimdall-content-click="cart.add"
  heimdall-payload-from="self"
  data-product-id="42"
  heimdall-content-target="#cart"
  heimdall-content-swap="outer">
  Add to cart
</button>

3. The Same Markup From C#

The fluent API keeps server-rendered markup strongly typed where useful, but the output is the same HTML contract.

button.Type("button");
button.Data("product-id", "42");
button.Heimdall()
    .Click("cart.add")
    .PayloadFromSelf()
    .Target("#cart")
    .SwapOuter();

Terminology Note

A content action is a server method registered with [ContentInvocation]. An <invocation> element is a response directive for out-of-band DOM updates.

The directive does not call a server action. It is consumed from HTML already returned by one.

4. Native HTML Attribute Helpers

Every element builder supports generic attributes, boolean attributes, classes, data attributes, and ARIA attributes. Use the named helpers when they exist and Attr or Bool for the rest of the HTML platform.

// Named helpers
input.Id("email");
input.Type(Html.InputType.email);
input.Name("Email");
input.Value(model.Email ?? "");
input.Placeholder("you@example.com");
input.AutoComplete("email");
input.Required();
input.MinLength(5);
input.MaxLength(100);
input.Pattern(".+@.+\\..+");

// Generic escape hatches
input.Attr("inputmode", "email");
input.Attr("aria-describedby", "email-help email-error");
input.Data("tracking-field", "email");
input.Bool("disabled", model.IsSaving);

Attribute values are encoded when rendered. Boolean attributes render by presence only when the supplied condition is true.

5. HTML Helper Cheat Sheet

These are the native HTML helper names exposed by the current server rendering API. When you need an attribute not listed here, use Attr or Bool.

Generic:
Attr(name, value)
Bool(name, on)
Class(classes...)
Id(value)
Style(css)
Role(value)
TitleAttr(value)
Data(key, value)
Aria(key, value)

Links and media:
Href(value)
Src(value)
Alt(value)
Rel(value)
Target(value)

Forms and inputs:
Type(value)
Type(Html.InputType.email)
Name(value)
Value(value)
Placeholder(value)
AutoComplete(value)
Min(value)
Max(value)
Step(value)
Pattern(value)
MaxLength(value)
MinLength(value)
Rows(value)
Cols(value)
Action(value)
Method(value)
EncType(value)
For(value)

Boolean:
Disabled()
Checked()
Selected()
ReadOnly()
Required()
Multiple()
AutoFocus()

6. Content Triggers

A trigger attribute names the server action to invoke. The value is the resolved ContentInvocation id.

heimdall-content-click="orders.refresh"
heimdall-content-submit="orders.save"
heimdall-content-input="search.query"
heimdall-content-change="filters.change"
heimdall-content-keydown="search.accept"
heimdall-content-blur="profile.validate-name"
heimdall-content-hover="products.preview"
heimdall-content-visible="feed.load-more"
heimdall-content-scroll="activity.load-earlier"
heimdall-content-load="dashboard.summary"

7. Trigger Helpers

The static helpers and fluent Heimdall wrapper emit the same trigger attributes. Use whichever shape is clearer for the file you are working in.

// Static attributes
HeimdallHtml.OnClick("orders.refresh")
HeimdallHtml.OnSubmit("orders.save")
HeimdallHtml.OnInput("search.query")
HeimdallHtml.OnChange("filters.change")
HeimdallHtml.OnKeyDown("search.accept")
HeimdallHtml.OnBlur("profile.validate-name")
HeimdallHtml.OnHover("products.preview")
HeimdallHtml.OnVisible("feed.load-more")
HeimdallHtml.OnScroll("activity.load-earlier")
HeimdallHtml.OnLoad("dashboard.summary")

// Fluent
element.Heimdall().Click("orders.refresh");
element.Heimdall().Submit("orders.save");
element.Heimdall().Input("search.query");
element.Heimdall().Change("filters.change");
element.Heimdall().KeyDown("search.accept");
element.Heimdall().Blur("profile.validate-name");
element.Heimdall().Hover("products.preview");
element.Heimdall().Visible("feed.load-more");
element.Heimdall().Scroll("activity.load-earlier");
element.Heimdall().Load("dashboard.summary");

8. Target and Swap

Target chooses the DOM element that receives returned HTML. Swap chooses how that HTML is applied. If target is omitted, the triggering element is the fallback target. If swap is omitted, inner is used.

<button
  heimdall-content-click="profile.refresh"
  heimdall-content-target="#profile-card"
  heimdall-content-swap="outer">
  Refresh
</button>

<section id="profile-card">
  ...
</section>

Supported swap values:

inner      -> replace target children
outer      -> replace the target element itself
beforeend  -> append inside the target
afterbegin -> prepend inside the target
none       -> do not apply the main response directly

9. Target and Swap Helpers

The helper names map directly to the target and swap attributes.

element.Heimdall()
    .Target("#profile-card")
    .Swap(HeimdallHtml.Swap.Outer);

element.Heimdall().SwapInner();
element.Heimdall().SwapOuter();
element.Heimdall().SwapBeforeEnd();
element.Heimdall().SwapAfterBegin();
element.Heimdall().SwapNone();

// Static helpers
HeimdallHtml.Target("#profile-card")
HeimdallHtml.SwapMode(HeimdallHtml.Swap.Outer)

10. Payload Attributes

Payload attributes tell the runtime what JSON body to send with the action request. Inline payload wins first, then payload-ref, then payload-from.

<!-- 1. Inline JSON object -->
<button
  heimdall-content-click="reports.load"
  heimdall-payload='{"page":2,"sort":"desc"}'>
  Load
</button>

<!-- 2. Global object path on window -->
<button
  heimdall-content-click="reports.load"
  heimdall-payload-ref="App.Reports.Filters">
  Refresh
</button>

<!-- 3. Source directive -->
<button
  heimdall-content-click="reports.load"
  heimdall-payload-from="closest-form">
  Search
</button>

11. Payload Source Values

Use the source that already owns the interaction data. Submit triggers also fall back to their form payload when no payload source is supplied.

closest-form
-> nearest containing form as FormData

self
-> the triggering element's dataset

closest-state
-> nearest data-heimdall-state JSON boundary

closest-state:weather
-> nearest data-heimdall-state-weather JSON boundary

#search-form
-> explicit selector that resolves to a form

ref:App.Reports.Filters
-> global object path on window

12. Payload Helpers

The helpers serialize objects when needed and emit the same payload attributes shown above.

element.Heimdall().Payload(new { page = 2, sort = "desc" });
element.Heimdall().PayloadEmptyObject();
element.Heimdall().PayloadFromClosestForm();
element.Heimdall().PayloadFromSelf();
element.Heimdall().PayloadFromClosestState();
element.Heimdall().PayloadFromClosestState("weather");
element.Heimdall().PayloadRef("App.Reports.Filters");
element.Heimdall().PayloadFromRef("App.Reports.Filters");

// Static helpers
HeimdallHtml.Payload(new { page = 2 })
HeimdallHtml.PayloadFromDirective("closest-form")
HeimdallHtml.PayloadRef("App.Reports.Filters")
HeimdallHtml.PayloadFromClosestState("weather")

13. State Attributes

State is just JSON stored on the DOM boundary that owns the current interaction context.

<div
  id="counter-host"
  data-heimdall-state='{"count":1}'>
  ...
</div>

<div
  id="weather-host"
  data-heimdall-state-weather='{"page":2,"zip":"48104"}'>
  ...
</div>

State helper equivalents:

host.Heimdall().State(new CounterState { Count = 1 });
host.Heimdall().State("weather", new WeatherState { Page = 2 });
host.Heimdall().StateJson("{\"count\":1}");
host.Heimdall().StateJson("weather", "{\"page\":2}");

// Static helpers
HeimdallHtml.State(new CounterState { Count = 1 })
HeimdallHtml.State("weather", new WeatherState { Page = 2 })

14. Modifier Attributes

Modifiers refine trigger behavior. They do not create a new action. They shape timing, filtering, browser defaults, in-flight behavior, and delegated trigger resolution.

heimdall-debounce="300"
-> delay input or change invocation until events settle

heimdall-key="Enter"
-> keydown only fires for Enter

heimdall-hover-delay="200"
-> hover only fires after pointer dwell

heimdall-visible-once="false"
-> visible may fire more than once

heimdall-scroll-threshold="160"
-> scroll fires near the end of the scroll region

heimdall-poll="5000"
-> repeat the load action every 5 seconds

heimdall-prevent-default="true"
-> prevent native browser behavior before invoking

heimdall-content-disable="true"
-> set disabled and aria-busy while request is in flight

heimdall-ignore="click"
-> stop delegated click resolution across this boundary

heimdall-scope="self"
-> trigger only when the event target is the trigger element

15. Modifier Helpers

These helper names emit the modifier attributes. Boolean Heimdall attributes accept presence, true, 1, or yes as true; false, 0, or no as false.

element.Heimdall().DebounceMs(300);
element.Heimdall().Key("Enter");
element.Heimdall().HoverDelayMs(200);
element.Heimdall().VisibleOnce(false);
element.Heimdall().ScrollThresholdPx(160);
element.Heimdall().PollMs(5000);
element.Heimdall().PreventDefault();
element.Heimdall().Disable();
element.Heimdall().Ignore(HeimdallHtml.Trigger.Click);
element.Heimdall().IgnoreAll();
element.Heimdall().ScopeSelf();
element.Heimdall().ScopeClosest();

// Static helpers
HeimdallHtml.DebounceMs(300)
HeimdallHtml.Key("Enter")
HeimdallHtml.HoverDelayMs(200)
HeimdallHtml.VisibleOnce()
HeimdallHtml.ScrollThresholdPx(160)
HeimdallHtml.PollMs(5000)
HeimdallHtml.PreventDefault()
HeimdallHtml.Disable()
HeimdallHtml.Ignore(HeimdallHtml.Trigger.Click)
HeimdallHtml.Scope(HeimdallHtml.EventScope.Self)

16. Runtime Defaults Worth Knowing

A few browser behaviors are intentionally useful without extra markup. Add the attributes when you want to make the behavior explicit or override it.

Default target:
triggering element

Default swap:
inner

Input debounce:
250ms unless heimdall-debounce is supplied

Change debounce:
0ms unless heimdall-debounce is supplied

Hover delay:
150ms unless heimdall-hover-delay is supplied

Visible once:
true unless heimdall-visible-once is false

Scroll threshold:
120px unless heimdall-scroll-threshold is supplied

Click and submit:
disable during request by default

Anchor click and form submit:
prevent default browser behavior by default

disabled or aria-disabled="true":
action does not run

17. SSE Attributes

SSE attributes subscribe an element to a Bifrost topic. The topic chooses the stream. The event chooses the message type. Target and swap choose how received HTML is applied.

<div
  id="orders-stream"
  heimdall-sse="orders"
  heimdall-sse-event="order.updated"
  heimdall-sse-target="#orders"
  heimdall-sse-swap="beforeend">
</div>

<!-- Alias for heimdall-sse -->
<div heimdall-sse-topic="orders"></div>

SSE defaults: event is heimdall, target is the subscriber element, swap is none, and disable is false.

element.Heimdall()
    .SseTopic("orders")
    .SseEvent("order.updated")
    .SseTarget("#orders")
    .SseSwap(HeimdallHtml.Swap.BeforeEnd);

element.Heimdall().SseDisable();

// Static helpers
HeimdallHtml.SseTopic("orders")
HeimdallHtml.SseTopicAlias("orders")
HeimdallHtml.SseEvent("order.updated")
HeimdallHtml.SseTarget("#orders")
HeimdallHtml.SseSwapMode(HeimdallHtml.Swap.BeforeEnd)
HeimdallHtml.SseDisable()

18. Response Directive Markup

Response directives are HTML elements returned by an action. The runtime consumes them as instructions and removes them from the final DOM.

<!-- Update another DOM region -->
<invocation
  heimdall-content-target="#toast-manager"
  heimdall-content-swap="afterbegin">
  <div class="toast">Saved</div>
</invocation>

<!-- Wrap parser-sensitive payloads such as table rows -->
<invocation
  heimdall-content-target="#orders-body"
  heimdall-content-swap="beforeend">
  <template>
    <tr><td>Order 1001</td></tr>
  </template>
</invocation>

<!-- Cancel the main swap but keep invocations -->
<abort reason="validation-failed"></abort>

<!-- Call an existing browser function -->
<javascript
  function="window.App.toast.success"
  args='["Saved"]'
  timing="after">
</javascript>

<!-- Navigate the browser -->
<redirect url="/login"></redirect>

19. Response Directive Helpers

Use the helpers from content actions so directive markup stays valid and easy to refactor.

return Html.Fragment(
    RenderForm(model),
    HeimdallHtml.Invocation(
        targetSelector: "#toast-manager",
        swap: HeimdallHtml.Swap.AfterBegin,
        payload: ToastManager.Create(toast)),
    HeimdallHtml.Abort("validation-failed")
);

return HeimdallHtml.JsInvokeVoid(
    "window.App.toast.success",
    "Saved");

return HeimdallHtml.Redirect("/login");

20. Complete Form Example

This is a complete native HTML form boundary. It uses normal browser validation attributes plus Heimdall submit and payload attributes.

Rendered HTML

<form
  id="contact-form"
  heimdall-content-submit="contact.submit"
  heimdall-payload-from="closest-form"
  heimdall-content-target="#contact-form"
  heimdall-content-swap="outer">
  <label for="contact-email">Email</label>
  <input
    id="contact-email"
    name="Email"
    type="email"
    autocomplete="email"
    required
    maxlength="100"
    aria-describedby="contact-email-help" />

  <small id="contact-email-help">
    Use an address we can reply to.
  </small>

  <label for="contact-message">Message</label>
  <textarea
    id="contact-message"
    name="Message"
    required
    minlength="10"
    maxlength="1000"
    rows="5"></textarea>

  <button type="submit">Send</button>
</form>

C# Builder

form.Id("contact-form");
form.Heimdall()
    .Submit("contact.submit")
    .PayloadFromClosestForm()
    .Target("#contact-form")
    .SwapOuter();

form.Label(label =>
{
    label.For("contact-email");
    label.Text("Email");
});

form.Input(Html.InputType.email, input =>
{
    input.Id("contact-email");
    input.Name("Email");
    input.AutoComplete("email");
    input.Required();
    input.MaxLength(100);
    input.Aria("describedby", "contact-email-help");
});

form.TextArea(textarea =>
{
    textarea.Id("contact-message");
    textarea.Name("Message");
    textarea.Required();
    textarea.MinLength(10);
    textarea.MaxLength(1000);
    textarea.Rows(5);
});

21. Complete Stateful Button Example

This is the smallest stateful component pattern: the host carries state, the button sends closest-state, and the response replaces the host.

<div
  id="counter-host"
  data-heimdall-state='{"count":1}'>
  <p>Count: 1</p>

  <button
    type="button"
    heimdall-content-click="counter.increment"
    heimdall-payload-from="closest-state"
    heimdall-content-target="#counter-host"
    heimdall-content-swap="outer">
    Increment
  </button>
</div>

22. Razor or MVC Partial Example

In MVC or Razor partials, write the attributes directly. The Heimdall runtime does not care whether the element came from Razor, a fluent builder, or a template.

@model OrderFilter

<form
  id="order-filter"
  heimdall-content-submit="orders.filter"
  heimdall-payload-from="closest-form"
  heimdall-content-target="#orders-table"
  heimdall-content-swap="inner">
  <input
    name="Search"
    type="search"
    value="@Model.Search"
    autocomplete="off"
    heimdall-content-input="orders.filter"
    heimdall-payload-from="closest-form"
    heimdall-debounce="300"
    heimdall-content-target="#orders-table"
    heimdall-content-swap="inner" />

  <button type="submit">Apply</button>
</form>

<div id="orders-table">
  @await Html.PartialAsync("_OrderTable", Model)
</div>

23. Implementation Checklist

When an interaction does not work, walk the markup in this order. Almost every issue becomes visible in the rendered HTML.

1. Is the runtime script loaded?
   /_content/HeimdallFramework.Web/heimdall-bundle.min.js

2. Is there exactly one trigger attribute with an action id?
   heimdall-content-click="orders.refresh"

3. Does the action id match the resolved ContentInvocation id?
   [ContentInvocationPrefix("orders")]
   [ContentInvocation("refresh")]
   -> orders.refresh

4. Does the payload source point to real data?
   closest-form, closest-state, self, selector, or ref:path

5. Does the target selector match an element?
   heimdall-content-target="#orders"

6. Is the swap mode appropriate?
   inner, outer, beforeend, afterbegin, or none

7. Is the element disabled?
   disabled or aria-disabled="true" prevents invocation