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 throughout2. 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.
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.
Next Steps
Once forms make sense, out-of-band updates and lazy loading become much easier to reason about.