*stage
Aliases: *stage, n-stage
Category: host-level data control
Summary
*stage enables a two-phase editing model on a Sercrod host.
Instead of mutating the host data directly, the runtime creates a deep copy called the staged buffer.
All bindings inside the host work against this staged buffer, and you can later commit or discard those changes using other directives such as *apply and *restore.
- Without
*stage: bindings read and write the host data (_data) directly. - With
*stage: bindings read and write a staged copy (_stage), and the host data is only updated when you explicitly apply changes.
The attribute value of *stage or n-stage is currently not evaluated. Presence alone enables staging.
Basic example
@@@html <serc-rod data={ user: { name: "Alice", email: "alice@example.com" } } *stage>
@@@In this configuration:
- Typing into the inputs updates the staged
userobject. - The host template is evaluated against the staged buffer.
- Clicking
*applycopies the staged values back to the host data. - Clicking
*restorediscards staged edits and resets the staged buffer from the last applied state.
Behavior
At runtime, Sercrod maintains two internal objects per host:
_data: the stable host data that represents the committed state._stage: an optional staged copy used while*stageis active.
When a Sercrod host is created:
_datais initialised from thedataattribute or inherited scope as usual.- If the host has
*stageorn-stage, and_stageis stillnull, the runtime creates a deep copy of_dataand stores it in_stage.
When the host renders:
- The effective scope for expressions is
this._stage ?? this._data. - If
_stageexists, all ordinary bindings and directives inside the host see only the staged copy. - If
_stageisnull, everything behaves like a normal, non staged host.
When inputs bound with *input or n-input fire:
- The runtime chooses
target = this._stage ?? this._data. - The binding expression is evaluated against
targetand then assigned back intotarget. - If
_stageexists, input bindings update the staged buffer, not the committed data.
Evaluation timing
*stage affects when and where data is cloned, but it does not introduce its own expression language or custom timing. The key timings are:
- During initialisation (
connectedCallback):- After
_datais prepared, if the host has*stageorn-stageand_stageis stillnull, Sercrod deep copies_datainto_stage.
- After
- During each
update():- For nested hosts that receive a parent scope via
__sercrod_scope, if the host has*stageorn-stageand__sercrod_scopeis set,_stageis refreshed from the inherited scope.
- For nested hosts that receive a parent scope via
- During input events:
*inputandn-inputalways write tothis._stage ?? this._data.- When
_stageis present, the input handlers skip automatic full host re render, because the form control already reflects the current user input.
No additional scheduling hooks are introduced by *stage itself. It changes which object is edited and rendered, not when expressions are evaluated.
Execution model
Conceptually, *stage splits the host data into:
- Stable state:
_data(committed, long lived). - Working state:
_stage(temporary, editable).
The execution model is:
- Host initialises
_data. - If
*stageis present:_stageis created as a deep copy of_data.- All template expressions inside the host see
_stage.
- While the user edits:
*inputandn-inputwrites go into_stage.- Other directives that mutate data (such as
*load) also prefer_stageover_data.
- When the user commits:
*applycopies_stageback into_dataand refreshes_stagefrom the new committed state.
- When the user discards:
*restorethrows away the current_stagecontents and recreates_stagefrom the last committed snapshot.
Internally, Sercrod first tries structuredClone to create _stage. If that fails, it falls back to JSON.parse(JSON.stringify(...)). This means *stage is primarily intended for JSON like data (plain objects, arrays, numbers, strings, booleans, and null).
Variable creation
*stage does not create new variables in the template scope. It only changes which data object is used as the root when resolving existing expressions.
- Identifiers like
user,form, and so on are still defined where they normally are. $root,$parent, and other special values behave as usual.- There is no additional binding such as
$stageor similar introduced by this directive in the current runtime.
In other words, the same expressions you would write without *stage continue to work. They now read and write from the staged buffer instead of the committed data when staging is active.
Scope layering
The behavior of *stage depends on whether the Sercrod host is a root host or a nested host.
-
Root host:
_datacomes from the hostdataattribute._stageis cloned from_dataonce during initialisation.- Subsequent
update()calls do not automatically overwrite_stagefrom_data. The staged buffer is kept until you explicitly overwrite or reset it via*apply,*restore, or other operations that rebuild_stage.
-
Nested host (child Sercrod with an inherited scope
__sercrod_scope):_datais a proxied view of the inherited scope.- On
update(), if*stageis present and__sercrod_scopeis available,_stageis refreshed from__sercrod_scope. - When you click
*apply, changes in_stageare written back into_data(and therefore into the parent scope), then_stageis refreshed from the new committed state.
In both cases, expressions inside the host are evaluated against this._stage ?? this._data. Parent scopes above the host are not changed by *stage unless you explicitly propagate changes using *apply on a nested host or other application specific code.
Interaction with bindings and data directives
*stage changes the behavior of several other directives that work on host data:
-
*input/n-input:- By default, bindings write to
this._stage ?? this._data. - With
*stageactive, all input writes go into_stage. - The input handler does not automatically trigger a full host re render when
_stageis present, so derived views are refreshed when you explicitly re render (for example via*apply,*restore, or*load).
- By default, bindings write to
-
*load/n-load:- When loading JSON, the runtime updates
_stageif it exists, otherwise_data. - With
*stage, this lets you load draft data without touching the committed state until you explicitly apply it.
- When loading JSON, the runtime updates
-
*post/n-post:- When serialising data for sending, the runtime uses
this._stage ?? this._dataas the source. - With
*stage, the posted payload is the staged buffer by default.
- When serialising data for sending, the runtime uses
-
*save/n-save:- When generating a downloadable JSON snapshot, the runtime again uses
this._stage ?? this._data. - With
*stage, the exported file reflects the current staged state.
- When generating a downloadable JSON snapshot, the runtime again uses
Other directives that only read data (such as *print, *textContent, or conditions and loops) automatically see the staged values when *stage is present, because they evaluate against the same effective scope.
Interaction with *apply and *restore
*stage is designed to be used together with *apply and *restore.
-
*apply:- Available on any descendant element inside a staged host.
- On click, if
_stageexists:- Copies the staged values into
_datausingObject.assign. - Calls
update()on the host. - Stores a snapshot of the committed
_datainto an internal_appliedbuffer used by*restore.
- Copies the staged values into
- Without
_stage(no*stageon the host),*applydoes nothing.
-
*restore:- Also available on any descendant element inside a staged host.
- On click:
- Only executes if the host has
*stageorn-stage. - Recreates
_stagefrom_appliedif available, otherwise from_data. - Calls
update()so that the view reflects the restored state.
- Only executes if the host has
- Without
*stage,*restorehas no effect.
This trio (*stage plus *apply plus *restore) is the recommended pattern for implementing editable but confirmable forms.
Use with conditionals and loops
*stage does not introduce any new restrictions on conditionals or loops. Instead, it transparently changes the data that those directives see:
*if,*for, and*eachevaluate their expressions against the effective scopethis._stage ?? this._data.- With staging enabled, these directives respond to staged values instead of committed values.
Examples:
- Show a banner when there are unsaved staged changes (assuming your data includes such a flag).
@@@html <serc-rod data={ form: { dirty: false } } *stage>
You have unsaved changes.
- Render a preview list based on staged filters and staged items.
@@@html <serc-rod data={ filter: "", items: [] } *stage> <input *input="filter">
<button type="button" *apply>Apply filter and items @@@
In both cases, the loop and conditional behavior is identical to non staged code, but they operate on staged values.
Best practices
-
Use
*stageat the host level for two phase edits:- Form drafts that should not be committed until an explicit click.
- Bulk edits that should be applied or discarded in one step.
- Import or load flows where the user reviews the loaded data before it becomes effective.
-
Keep staged data JSON like:
*stageuses a deep clone based onstructuredClonewith a JSON fallback.- Avoid storing functions, DOM nodes, or complex non serialisable objects in the part of the data you expect to stage.
-
Combine with
*applyand*restore:- Always provide a clear commit button (
*apply) and, ideally, a reset button (*restore) when using staging. - This makes the two phase model obvious in the UI.
- Always provide a clear commit button (
-
Be explicit about when the view refreshes:
- With
*stage, input handlers do not trigger a full host re render on each keystroke. - If you need derived non input elements to update live based on staged changes, call
update()from your own methods or trigger operations (*load,*apply,*restore) that callupdate()internally.
- With
-
Restrict
*stageto Sercrod hosts:- In the current runtime, only Sercrod host elements (such as
<serc-rod>or other Sercrod based custom elements) actually interpret*stageorn-stage. - Adding
*stageto ordinary elements has no effect from Sercrod’s perspective.
- In the current runtime, only Sercrod host elements (such as
Additional examples
Simple staged form with save and reset:
@@@html <serc-rod data={ profile: { name: "", bio: "" } } *stage>
Edit profile (staged)
Committed name:
Staged name:
<button type="button" *apply>Apply to committed profile <button type="button" *restore>Discard staged changes @@@
Staged load and post flow:
@@@html <serc-rod data={ form: {} } *stage> <input type="file" *load> <button type="button" *post="/api/preview">Send staged form to preview API
<button type="button" *apply>Apply staged form to committed data @@@
Here:
*loadmerges the loaded JSON into_stagerather than_data.*postsends the staged data to the server.- Only when
*applyis clicked does the committed data change.
Notes
*stageis a host level flag. It does not have per field granularity in the current implementation. Once activated, all host level data that bindings touch are read and written via the staged buffer.- The attribute value of
*stageorn-stageis currently ignored. Use it only as a presence flag. Existing examples that show*stage="draft"are effectively equivalent to*stage. - Without
*stage, directives such as*applyand*restoresafely do nothing, so you can keep button markup in place and enable staging later by adding the host flag. - Because the staged buffer is a deep copy, large data structures may have a cost when staging is first enabled or when a nested staged host resynchronises from its parent scope. Design your data shape accordingly.