Heimdall Docs

Forms

Heimdall forms stay HTML-first. The browser sends form data to a server action, the server validates and re-renders the current truth, and Heimdall updates the UI with returned HTML.

That can look like a traditional submit flow, or a more interactive validation loop. Both are valid Heimdall patterns.

1. Two Common Form Styles

Heimdall supports both traditional forms and highly interactive forms. The difference is not whether the server is involved. The difference is how often the form talks to the server and how much of the UI is refreshed during the process.

Traditional form:
- submit once
- return success or error UI
- optionally update other regions out-of-band

Interactive form:
- validate during input or change
- re-render the form host as needed
- keep the server as the source of truth throughout

2. Shared Foundation

Both styles still use the same Heimdall building blocks: a form boundary, payload binding from the closest form, a content action, and returned HTML.

form.Heimdall()
    .OnSubmit("submit-message")
    .PayloadFromClosestForm();

3. Traditional Form Submit

A traditional Heimdall form behaves a lot like a normal form submission, except the response can still update targeted UI regions using HTML. This is a great fit for contact forms, feedback forms, and simple one-shot workflows.

Strongly Typed Form

return Form(form =>
{
    form.Heimdall()
        .OnSubmit("submit-message")
        .PayloadFromClosestForm()
        .SwapNone();

    form.Id("contactForm");

    form.Add(NameInput);
    form.Add(EmailInput);
    form.Add(PhoneInput);
    form.Add(MessageInput);

    form.Div(statusMsg =>
    {
        statusMsg.Id("statusMessage");
    });

    form.Add(SubmitButton_Render());
});

Rendered HTML Mental Model

<form
  heimdall-submit="submit-message"
  heimdall-payload="closest-form"
  heimdall-swap="none"
  id="contactForm">
  ...
  <div id="statusMessage"></div>
  <button type="submit">Submit</button>
</form>

Action

[ContentInvocation("submit-message")]
public static async Task<IHtmlContent> SubmitMessageAsync(
    EmailService email,
    ContactFormSubmission submission)
{
    bool isSuccess = await email.SendAsync(
        subject: $"New contact form submission from {submission.Name}",
        body: $"Name: {submission.Name}\\nEmail: {submission.Email}\\nPhone: {submission.Phone}\\nMessage: {submission.Message}"
    );

    string target = isSuccess ? "#contactForm" : "#statusMessage";

    return HeimdallHtml.Invocation(
        targetSelector: target,
        swap: HeimdallHtml.Swap.Inner,
        payload: HandleStatusMessage(isSuccess)
    );
}

4. Why SwapNone Works Well Here

In a traditional submit flow, the main form submission does not always need a direct primary swap target. Instead, the action can decide what region to update by returning an invocation that targets the form, a status message region, or some other part of the layout.

form.Heimdall()
    .OnSubmit("submit-message")
    .PayloadFromClosestForm()
    .SwapNone();

5. Traditional Forms Can Still Use Native HTML Validation

This style pairs very naturally with standard HTML form features like required, minlength, maxlength, pattern, and type=email. Heimdall does not fight the platform here.

nameInput.Required();
nameInput.MinLength(5);
nameInput.MaxLength(50);
nameInput.Pattern("[A-Za-z]+( [A-Za-z]+)*");

emailInput.Type("email");
emailInput.Required();

msgInput.Required();
msgInput.MinLength(5);
msgInput.MaxLength(500);

6. Interactive Server-Driven Forms

Heimdall also supports more interactive forms where the server re-validates and re-renders the form as the user types, changes fields, or submits. This is a great fit when the server needs to continuously own validation and UI truth.

Interactive Trigger

button.Heimdall()
    .Click("Notes.Create")
    .PayloadFromClosestForm()
    .Target("#create-note-host")
    .SwapInner();

Live Validation Example

input.Heimdall()
    .Input("Notes.Validate")
    .PayloadFromClosestForm()
    .Target("#create-note-host")
    .SwapInner();

DTO

public sealed class CreateNoteRequest
{
    public string? Title { get; set; }
    public string? Body { get; set; }
    public bool IsDirty { get; set; }
}

Action

[ContentInvocation]
public static IHtmlContent Create(CreateNoteRequest request)
{
    var errors = Validate(request);

    if (errors.Any())
    {
        return RenderForm(request, errors);
    }

    SaveNote(request);
    return RenderForm(new CreateNoteRequest(), []);
}

7. Why Inner Swap Is a Good Default for Interactive Forms

Interactive forms usually work best with a stable host boundary. Inner swap keeps the host in place while replacing only the server-rendered contents of the form region.

<div id="create-note-host">
  <!-- server-rendered form -->
</div>

8. Comparing the Two Approaches

Both styles are valid. Choose the one that matches the user experience you want.

Traditional Submit

  • Best for simple, one-shot submissions
  • Works naturally with native HTML validation
  • Often uses SwapNone plus targeted invocation updates
  • Good for contact, support, and feedback forms

Interactive Server-Driven

  • Best when the server should continuously own validation state
  • Often re-renders a stable form host with SwapInner
  • Works well for richer workflows and inline feedback
  • Good for editors, wizards, and server-driven validation loops

9. The Validation Loop

No matter which style you choose, the core loop is the same.

1. The browser sends form values from the closest form.
2. The server normalizes and validates the request.
3. The server returns HTML representing the current truth.
4. Heimdall updates the relevant UI boundary.

10. Updating Other UI Regions

A form submission can update more than one place. For example, the main form can be refreshed while a list or toast region is updated out-of-band.

[ContentInvocation]
public static IHtmlContent Create(CreateNoteRequest request)
{
    SaveNote(request);

    return Html.Fragment(
        RenderForm(new CreateNoteRequest(), []),
        RenderNotesListInvocation(),
        RenderSuccessToastInvocation());
}

11. Common Pattern: Form Host + Related Regions

A common Heimdall pattern is to keep the form in one host and update other regions separately, such as lists, status areas, or toast containers.

<div id="create-note-host"></div>
<div id="notes-host"></div>
<div id="statusMessage"></div>
<div id="toast-manager"></div>

12. Why This Works Well

Heimdall forms work well because the same server code owns validation, normalization, persistence, and rendering.

The server remains the source of truth.
The browser only needs standard HTML and Heimdall attributes.
The returned markup always reflects current application truth.

Next Steps

Once forms make sense, out-of-band updates and lazy loading become much easier to reason about.