*input
Summary
*input binds a form control’s value to a data path. It keeps a DOM control (such as <input>, <textarea>, or <select>) in sync with a field in Sercrod’s data or staged data. The alias n-input behaves identically.
*input is focused on value-level binding and works together with *lazy and *eager to control how often the host re-renders after a change.
Basic example
A simple text field bound to form.name:
<serc-rod id="app" data='{"form":{"name":"Alice"}}'>
<label>
Name:
<input type="text" *input="form.name">
</label>
<p>Hello, <span *print="form.name"></span>!</p>
</serc-rod>
Behavior:
- On initial render, the
<input>shows"Alice". - When the user edits the field,
form.nameis updated. - The
<span *print="form.name">reflects the new value after Sercrod’s update cycle.
Behavior
*input provides a two-way binding between a control and a data expression:
- Data to UI:
- On render, Sercrod evaluates the expression from
*inputand sets the control’s current value, checked state, or selection.
- On render, Sercrod evaluates the expression from
- UI to data:
- When the user interacts with the control, Sercrod writes the new value back to the expression using
assign_expr. - Optional filters (
model_outandinput_in) can transform data when rendering or capturing input.
- When the user interacts with the control, Sercrod writes the new value back to the expression using
- Staged vs live data:
- If the host has staged data (
this._stage),*inputwrites into the staging area. - If not,
*inputwrites directly into the main data.
- If the host has staged data (
Supported tags:
INPUTTEXTAREASELECT
Other elements with a .value property may technically work, but *input is designed primarily for standard form controls.
Expression syntax
The *input expression is evaluated as a normal Sercrod expression and used as an assignment target:
-
Typical pattern:
*input="form.name"*input="user.profile.email"*input="settings.volume"
-
The left-hand side can be:
- A simple identifier (
username). - A dotted path (
form.name,user.settings.theme). - Any expression that is valid on the left of an assignment inside
with(scope){ ... }.
- A simple identifier (
Assignment semantics:
- Sercrod’s
assign_expr(lhs, value, scope):- Uses a sandboxed
with(scope){ lhs = __val }. - For unknown top-level identifiers, it allocates nested objects on demand.
- For example,
*input="profile.name"on an initially empty data object will createdata.profileif needed.
- For example,
- If the assignment fails, Sercrod logs a warning (when warn logging is enabled) and keeps the UI functional.
- Uses a sandboxed
Best practice:
- Use simple property paths for
*input(for exampleform.nameoruser.email). - Avoid complex arbitrary expressions on the left-hand side, as they may be hard to reason about in assignment.
Evaluation timing
*input participates in two phases:
-
Initial reflection (data to UI):
- During render, after Sercrod has prepared the scope, it:
- Evaluates the
*inputexpression on either staged or main data. - Applies the value to the control:
- For
input[type="checkbox"]: setscheckedfrom a boolean or array value. - For
input[type="radio"]: setscheckedbased on equality against the bound value. - For other inputs and textarea: sets
.value. - For select: sets the selected option(s).
- For
- Evaluates the
- During render, after Sercrod has prepared the scope, it:
-
Event-driven updates (UI to data):
- Sercrod attaches event listeners:
inputfor text-like controls.changefor checkboxes, radios, selects, and others.
- On those events, it:
- Normalizes the new value (including optional numeric conversion).
- Passes the value through
input_infilter. - Writes the result back via
assign_expr. - Triggers either a full update or a more focused child update depending on
*lazyand*eager, and whether staging is active.
- Sercrod attaches event listeners:
If the host has staged data, updates do not trigger a live host re-render until the staged changes are applied.
Execution model
A simplified execution model for *input:
-
Resolve binding:
- Sercrod obtains the expression string from
n-inputor*input. - It chooses a target object:
- Prefer staged data (
this._stage) when present. - Otherwise use main data (
this._data).
- Prefer staged data (
- It tests evaluation:
- If evaluating the expression on staged or main data throws a
ReferenceError, Sercrod falls back to the current scope object. - This allows bindings to work when the path only exists in the scope, not yet in the main data.
- If evaluating the expression on staged or main data throws a
- Sercrod obtains the expression string from
-
Initial data to UI:
- Evaluate the expression to get
curVal. - For different controls:
INPUT:type="checkbox":- If
curValis an array, the checkbox is checked when itsvalueis included in that array (string comparison). - Otherwise, the checkbox is checked when
curValis truthy.
- If
type="radio":- After rendering, a
postApplyhook re-evaluates the bound expression and setscheckedwhen the bound value equals the control’svalue.
- After rendering, a
- Other types:
curValis passed throughmodel_out(if defined), then normalized:null,undefined, andfalsebecome an empty string.
- The resulting string is assigned to
el.value.
TEXTAREA:- Similar to text inputs:
nullandfalsenormalize to an empty string.model_out(if defined) can transform the value before it is written.
- Similar to text inputs:
SELECT:- For
multiple:- Expects
curValas an array of values (strings after normalization). - A
postApplyhook selects all options whose value is contained in that array.
- Expects
- For single select:
- Writes a string value, with
nullmapped to an empty string.
- Writes a string value, with
- For
- Evaluate the expression to get
-
IME composition handling:
- For text-like controls (
inputexcept checkbox/radio, andtextarea), Sercrod tracks IME composition:- While composing (between
compositionstartandcompositionend),inputevents are ignored. - After composition ends, normal
inputhandling resumes.
- While composing (between
- For text-like controls (
-
UI to data (input events):
- For
input(text-like) andtextarea:- On each
inputevent, if not composing:- Read
el.valueasnextVal. - Align numeric types:
- If
type="number", convert to a number when possible, keeping empty string as empty. - Otherwise, if the current bound value is a number and the new value is not empty, try to parse a number.
- If
- Pass
nextValthroughinput_infilter. - Assign the result back to the expression via
assign_expr. - If there is no staged data:
- If
*eageris active, callupdate()on the host. - Otherwise, call
_updateChildren(...)to update only the host’s descendants.
- If
- Read
- On each
- For
-
UI to data (change events):
- For
changeon all relevant controls:- Compute
nextValdepending on control type:- Checkbox:
- If the bound value is an array, toggle membership of
el.valuein that array. - Otherwise, use
el.checkedas a boolean.
- If the bound value is an array, toggle membership of
- Radio:
- If
el.checked, useel.value.
- If
- Select:
- For
multiple, use an array of selectedoption.value. - Otherwise, use
el.value.
- For
- Others:
- Use
el.value.
- Use
- Checkbox:
- If the original bound value is a number (and
nextValis not empty and not an array), try to convert to number. - Pass
nextValthroughinput_inand assign to the binding expression. - If there is no staged data:
- If
*lazyis active, update only the host’s children. - Otherwise, perform a full
update()of the host.
- If
- Compute
- For
-
Staged data:
- When the host uses
*stage,*inputwrites intothis._stageinstead ofthis._data. - Because the update logic checks
if (!this._stage), staged inputs do not trigger automatic re-rendering of the live view. - Applying or restoring the stage (for example via
*apply/*restore) will drive the next visible update.
- When the host uses
Variable creation and scope layering
*input does not create new variables in the template scope.
Scope behavior:
- The binding expression is evaluated against:
- Staged data (
this._stage) or main data (this._data) as primary scopes. - The current scope as fallback on certain reference errors.
- Staged data (
- When writing,
assign_exproperates on the chosen scope and can create missing path segments. - All existing scope entries (such as
$data,$root,$parent, and variables from*let) remain available but are not modified unless your*inputexpression explicitly targets them.
Guidelines:
- Prefer to bind to stable data paths derived from
dataon<serc-rod>. - Avoid binding directly to transient local variables created by
*letunless you understand the implications.
Parent access
*input does not provide its own parent helper, but you can target parent data explicitly:
- Use normal paths to reach parent data structures (
parentForm.name,wizard.steps[current].value, and similar). - Use
$rootand$parentinside expressions if you need to bind to root-level or parent-host data.
The binding still obeys the same assignment semantics: the expression is the left-hand side of an assignment inside the current scope.
Use with conditionals and loops
*input can be freely combined with conditional rendering and loops, as long as the rendered control remains a valid form element:
-
With
*if:<label *if="form.enableName"> Name: <input type="text" *input="form.name"> </label>- When the condition becomes falsy, the input is removed from the DOM; when it becomes truthy again, the control is re-created and bound using the current data.
-
With
*for:<serc-rod id="list" data='{ "tags":["HTML","CSS","JavaScript"] }'> <ul> <li *for="tag of tags"> <label> <input type="checkbox" *input="selectedTags" :value="tag"> <span *print="tag"></span> </label> </li> </ul> <p>Selected: <span *print="selectedTags.join(', ')"></span></p> </serc-rod>- Here,
selectedTagsis expected to be an array. - Each checkbox toggles its tag inside that array.
- Here,
-
With
*each:*inputworks as expected inside bodies of*each, just like with*for.- The loop variables are visible to binding expressions.
Use with *stage, *lazy and *eager
*input is designed to integrate with staged editing and timing control directives.
-
With
*stage:<serc-rod id="profile" data='{"profile":{"name":"Taro","email":"taro@example.com"}}'> <form *stage="'profile'"> <label> Name: <input type="text" *input="profile.name"> </label> <label> Email: <input type="email" *input="profile.email"> </label> <button type="button" *apply="'profile'">Save</button> <button type="button" *restore="'profile'">Reset</button> </form> </serc-rod>- Inside the staged area,
profilerefers to staged data understage.profile. - Inputs modify only the staged copy, not live data.
- Applying or restoring the stage controls when the live data changes.
- Inside the staged area,
-
With
*lazy:*lazyandn-lazyadjust how change-driven updates behave:- When active, change events (for example from checkbox, radio, select) update the data but trigger a lighter child-only update instead of a full host re-render.
- The attribute can be used with or without a value:
*lazyor*lazy=""means “lazy is enabled”.*lazy="expr"is evaluated; if the expression is truthy, lazy behavior is enabled.
-
With
*eager:*eagerandn-eageraffect text-style inputs and textarea oninputevents:- When
*eageris enabled, text changes cause a fullupdate()of the host. - Without
*eager, Sercrod only updates the host’s children, which can be cheaper.
- When
- Similar to
*lazy, it can be:- Present as a bare attribute (
*eager). - Or have an expression value (
*eager="expr") that decides whether eager behavior is active.
- Present as a bare attribute (
Project-level recommendation:
- Use
*eagersparingly, only when the host needs to react immediately to every keystroke. - Use
*lazywhen you want heavy recomputation to happen on change rather than on every adjustment, especially for checkboxes, radios, and selects.
Best practices
-
Use simple, stable paths:
- Favor expressions like
form.nameoruser.email. - Avoid writing to very complex expressions; keep assignments intuitive.
- Favor expressions like
-
Initialize data types:
- When you expect a number, initialize the bound value as a number or use
type="number". - When you expect a checkbox group, initialize the bound value as an array.
- For multi-selects, also use arrays.
- When you expect a number, initialize the bound value as a number or use
-
Use filters for formatting and parsing:
- Implement
Sercrod._filters.model_outto format bound values before they appear in controls. - Implement
Sercrod._filters.input_into parse or validate input before storing it in data (for example trimming whitespace or coercing to domain-specific types).
- Implement
-
Combine with
*stagefor editing flows:- Wrap groups of inputs in staged sections when you need explicit “Save” / “Cancel” flows.
- Let
*applyand*restorecontrol when staged values become live.
-
Be mindful of update costs:
- Use
*eageronly when necessary; eager updates can be expensive on large templates. - Use
*lazyto limit full rerenders from frequently changing inputs.
- Use
-
Treat
nameattributes as optional:*inputbinds via its expression, not vianame.- You can still use
namefor browser-level features (such as form submission), but the binding does not rely on it.
Additional examples
Checkbox bound to a boolean:
<serc-rod id="flag" data='{"settings":{"enabled":true}}'>
<label>
<input type="checkbox" *input="settings.enabled">
Enabled
</label>
<p>Status: <span *print="settings.enabled ? 'ON' : 'OFF'"></span></p>
</serc-rod>
Checkbox group bound to an array:
<serc-rod id="colors" data='{"colors":["red","green","blue"],"selected":["red"]}'>
<div *for="color of colors">
<label>
<input type="checkbox" *input="selected" :value="color">
<span *print="color"></span>
</label>
</div>
<p>Selected: <span *print="selected.join(', ')"></span></p>
</serc-rod>
Radio group bound to a single value:
<serc-rod id="plan" data='{"plan":"basic"}'>
<label>
<input type="radio" name="plan" *input="plan" value="basic">
Basic
</label>
<label>
<input type="radio" name="plan" *input="plan" value="pro">
Pro
</label>
<p>Current plan: <span *print="plan"></span></p>
</serc-rod>
Select with multiple:
<serc-rod id="multi" data='{
"allOptions":["A","B","C"],
"chosen":["A","C"]
}'>
<label>
Choose:
<select multiple *input="chosen">
<option *for="opt of allOptions" :value="opt" *print="opt"></option>
</select>
</label>
<p>Chosen: <span *print="chosen.join(', ')"></span></p>
</serc-rod>
Notes
*inputandn-inputare aliases; choose one style and use it consistently.*inputis designed for<input>,<textarea>, and<select>.- The bound expression is treated as an assignment target; Sercrod will attempt to create missing intermediate objects when necessary.
*lazyand*eagerare optional helpers for tuning update timing; they do not change the core binding semantics.- IME composition is respected for text-like fields, so partial compositions do not repeatedly overwrite data.
- As with other directives,
*inputfollows Sercrod’s general rules for expressions, filters, and scope resolution; there are no special restrictions on combining it with other directives on the same element, as long as the result is still a well-formed form control.