Summary#
A page that edits rows already has a unit of work — the route's — and every field bound to a row joins it. What the page still has to decide is how to OFFER to keep the changes. The UI kit ships three answers, each a control you drop in, each reading and driving that same unit of work:
| control | what the person sees | reach for it when |
|---|---|---|
SaveBar | a bar rises at the foot while there are changes: Undo · Discard · Save | a page of many edits a person reviews before keeping — settings, terms, a long form |
SaveActions | ● Edited · Undo · Discard · Save inline, beside the record's name or in a drawer's head | a record edited in place, a side panel, a dense console |
AutoSave | Saving in 2 s · Undo, then All changes saved — there is no Save | a note, a CRM record, anything edited in passing |
entity Contact {
[Required, MaxLength(120)] string Name;
[MaxLength(400)] string? Note;
security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}
// Inline: Save and Discard beside the name, only while there is something to keep.
[Route("/contacts/{id}")] [Render(CSR)] [AllowAnonymous]
component ContactPage(Guid id) {
live var contact = Contact.FirstOrDefault(c => c.Id == id);
render {
if (contact != null) {
Stack(gap: 4, p: 6) {
PageHead(contact.Name) { SaveActions(saved: "Contact saved."); }
TextAreaField("Note", value: contact.Note, rows: 4);
}
}
}
}
// Kept by itself: no Save at all, and an Undo while the change waits.
[Route("/contacts/{id}/jot")] [Render(CSR)] [AllowAnonymous]
component ContactJot(Guid id) {
live var contact = Contact.FirstOrDefault(c => c.Id == id);
render {
if (contact != null) {
Stack(gap: 4, p: 6) {
PageHead(contact.Name) { AutoSave(after: 2); }
TextAreaField("Note", value: contact.Note, rows: 4);
}
}
}
}Signature#
SaveBar(Action onSave = null, string saved = "Saved.", string blockedBy = "", Action onSaved = null)
SaveActions(string saved = "Saved", string blockedBy = "", Action onSaved = null)
AutoSave(int after = 2, string saved = "All changes saved")Description#
What do all three have in common?#
They read Navigation.IsDirty to know whether there is anything to keep, and they act through UnitOfWork:
Commit() to keep, Discard() to drop every change, Undo() to step back one step. The platform takes a step for
every action the person runs and every field they type into, named by it ("Undo artwork"); a
UnitOfWork.Mark("Rename the status") names a step itself and groups the ones after it ([[ui-page-unit-of-work#undo]]).
Nothing about how the page EDITS changes between them — the fields bind rows exactly as before — so moving a page
from one to another is the one line that names the control.
A save the model's own rules would refuse is HELD, not sent: the edits stay, every field in the way is shown, and the control says how many ("1 field needs attention."). A save the SERVER refuses is never silent either — its reason is shown where the control is ("Not saved — …") — but what that commit carried is gone, so the page shows the stored row again ([[ui-page-unit-of-work#held-save]]).
And the edits are not lost on the way out: while the page is dirty, leaving it asks first — Keep editing, Save and leave, or Discard and leave — in the kit shells' own look, and from the platform for any other way out (LeavePrompt — asking before a page with unsaved edits is left, [[ui-page-unit-of-work#leaving]]).
Which one fits my page?#
Ask what a person expects to happen when they look away.
- They expect nothing to be kept until they say so — finance, settings, anything with consequences:
SaveBar. The bar appears only when there is something to keep, says so in words, and carries Mod+S and Mod+Z. - They are working in one record and want to keep it right there — a drawer, an inspector, a record's head:
SaveActions. It takes no room at all while there is nothing to keep, then shows the same three verbs inline. - They expect it to be kept, as a document editor keeps a document —
AutoSave. Once the person PAUSES forafterseconds (two by default) the change is kept; every further edit starts the count again (UnitOfWork.EditCount()), so nothing is kept mid-word. Until then it says so and can be taken back.
What does Undo mean once something saves itself?#
AutoSave's Undo is honest about time: while the change is waiting it steps back to the last savepoint (or, with
none, drops the changes not yet kept). Once the change is kept it is kept — the window to change your mind is the
one before. That is why the waiting state is visible and counts down rather than happening silently.
How do I hold a save back until the page is complete?#
SaveBar and SaveActions take blockedBy: — the sentence saying why the page cannot be kept as it stands. While
it is not empty the control says it and Save does nothing (Mod+S included). Compute it from the page's own values so
it changes as the person types.
How do I drive them in a test?#
entity Note {
[Required, MaxLength(60)] string Title;
security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}
[Route("/notes/{id}")] [Render(CSR)] [AllowAnonymous]
component NotePage(Guid id) {
live var note = Note.FirstOrDefault(n => n.Id == id);
render {
if (note != null) {
Stack(gap: 3, p: 6) {
PageHead("Note") { SaveActions(saved: "Kept."); }
Field("Title", value: note.Title);
}
}
}
}
[Test]
void an_inline_save_keeps_the_edit() {
var n = new Note { Title = "Draft" };
UnitOfWork.Commit();
Ui.Visit("/notes/" + n.Id);
Ui.Fill("Title", "Final");
Assert.Visible("Edited");
Ui.Click("Save");
Assert.Visible("Kept.");
Assert.Equal("Final", Note.Single().Title);
}For AutoSave, make the edit, Assert.Visible("Saving in"), then Ui.Tick() once per second of after — the
control's clock runs on on every, which a test fires on demand rather than waiting for it.
See also#
- A page's unit of work — what the platform does for an editing page, end to end — the whole journey: automatic undo, a held save, leaving, an act that waits
- LeavePrompt — asking before a page with unsaved edits is left — what happens when somebody leaves the page before keeping its edits
- SettingsArea — an app's settings: sections, explicit save and discard, undo, who changed what — the settings area, whose pages use
SaveBar - Dock — a docked drawer, where
SaveActionsbelongs - Arrangements — five ways to build a whole app, and which look wears each — which save presentation each archetype chose, and why
- creating & saving data — how an edit joins the unit of work in the first place