How Sercrod works
This page explains the runtime behavior that affects Sercrod templates, user interaction, data flow, and server communication.
It is not a philosophy page. It is a practical guide for reading and writing Sercrod HTML without losing track of how the page behaves.
HTML attributes define behavior
Sercrod uses HTML attributes to make the behavior of a page visible from the template.
This does not mean that every piece of logic must be written directly inside HTML. Small expressions can be written in attributes, but reusable logic, formatting, validation, and project-specific behavior can still live in JavaScript functions.
The role of Sercrod attributes is to show where behavior is connected.
For example, *input shows which data path receives an input value.
*print shows which value is rendered as text.
:class and :href show which DOM attributes are produced from data.
@click shows that a user action changes data or calls a function.
This makes the template easier to inspect. A developer, maintainer, or AI tool can look at the HTML and understand the main flow: which input writes to which data path, which value is displayed, which event is handled, and which function is called.
When logic becomes larger than a clear inline expression, it is usually better to move that logic into a named function and call it from the attribute. In that case, the HTML still shows the connection point, while the function keeps the detailed logic readable and reusable.
Example
<serc-rod data='{"email":"","message":""}'>
<input type="email" *input="email">
<textarea *input="message"></textarea>
<button @click="submit_form(email, message)">Send</button>
<p>
Email:
<span *print="email"></span>
</p>
</serc-rod>
In this example, *input="email" shows that the input value is written to
email. *input="message" shows that the input value is written to
message. @click shows that
submit_form(email, message) is called when the button is clicked.
*print="email" shows that email is also used for display.
Sercrod attributes are therefore not a replacement for functions. They are the visible contract between the template, data, user actions, and optional JavaScript functions.
Reading the data flow from the template
A Sercrod template shows how data moves between form controls, data paths, and rendered output.
When you read a Sercrod template, look for three things:
- where a value is written
- where the same value is read
- where the result appears in the DOM
Example
<serc-rod data='{"email":"","notice":""}'>
<input type="email" *input="email">
<button @click="notice = 'Sent to ' + email">Send</button>
<p *print="notice"></p>
</serc-rod>
In this example, *input="email" writes the input value to email.
The @click expression reads email and writes a new value to
notice. The *print="notice" element reads notice
and displays it as text.
The template does not only show the final HTML. It also shows the path that connects user input, data changes, and rendered output.
This makes Sercrod templates easier to review. You can inspect a small block and answer practical questions such as:
- Which input changes this value?
- Which expression writes this message?
- Which element displays the result?
- If I rename this data path, which attributes must change with it?
Sercrod does not remove the need for JavaScript functions or server endpoints. It keeps the local data flow visible at the place where the UI is written.
Input update timing
Sercrod form controls are usually connected to data with *input.
The important point is not only where the value is written. It is also when the surrounding template should react to that value.
With a normal *input, the input value is written back to data when the edit is
committed. A common commit point is focus-out: the user types in the input, then clicks
somewhere outside the input.
Normal input update
<serc-rod data='{"name":"Alice"}'>
<input type="text" *input="name">
<p>
Preview:
<span *print="name"></span>
</p>
</serc-rod>
In this example, the input writes to name, and the preview reads the same
name. When the user finishes editing and clicks outside the input, the input
loses focus. The edited value is committed, and the preview can update through the normal
template update flow.
This is useful when clicking outside the input is mainly a way to confirm the edit and update another part of the UI.
However, the place outside the input may also be a button.
For example, the user may type a message and then click Send. From the user's point of view, they clicked Send.
However, when the parent template is updated as part of the input commit, that first click is treated first as the input's focus-out and commit operation. The value is written back to data, and the parent template updates. The Send action itself does not run on that first click.
The user must click Send again to actually send the message.
This is safe because the typed value is preserved. But it is not natural from the user's point of view: the user intended to send the message, not just commit the input.
Using *lazy
<serc-rod data='{"message":""}'>
<input type="text" *input="message" *lazy>
<button @click="send_message(message)">Send</button>
</serc-rod>
*lazy keeps the input value available to the data path, but avoids an immediate
parent template refresh caused by each edit.
It does not mean that the value is ignored. It also does not simply mean "update on blur".
In this example, the user can type a message and then click Send naturally. The button can
still use message, but the input edit itself does not immediately force the
parent template to refresh before the next action.
Using *eager
*eager is for the opposite kind of interface.
Use *eager when the surrounding UI should react immediately while the user types,
such as live search, live filtering, live preview, counters, or immediate validation messages.
<serc-rod data='{"keyword":""}'>
<input type="text" *input="keyword" *eager>
<p>
Searching for:
<strong *print="keyword"></strong>
</p>
</serc-rod>
In this example, the visible text should change while the user is typing. That is why
*eager is appropriate.
The choice is about interaction behavior.
- Use normal
*inputwhen the normal commit and template update flow is enough. - Use
*lazywhen the user is expected to type first and then move focus or click a button. - Use
*eagerwhen the UI should react immediately while the user types.
*fetch / *api trigger timing
*fetch and *api are not only about which URL to call. Their timing
also depends on the element they are attached to.
When *fetch or a GET-style *api is placed on a non-clickable element,
Sercrod treats it as initial data loading. The request runs automatically when the element
is rendered.
Initial loading with *fetch
<serc-rod data='{"items":[]}' *fetch="/api/items.json:items">
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
In this example, the data is loaded when the Sercrod element is rendered. The user does not need to click anything. This is suitable for page-load data such as lists, navigation data, posts, or public JSON content.
Initial loading with *api
<serc-rod data='{"items":[]}' *api="/api/items.json" *into="items">
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
Here, *api loads the JSON response and stores it in items.
When *fetch or *api is placed on a clickable element, Sercrod treats
it as an action. The request runs when the user clicks the element.
Click-triggered loading
<serc-rod data='{"items":[]}'>
<button type="button" *fetch="/api/items.json:items">
Load items
</button>
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
In this example, the request does not run just because the page is displayed. It runs when the user clicks the button.
The difference between *fetch and *api
*fetch is the simpler directive. Use it when the main purpose is to load JSON and
place the result into data.
*api is the more general directive. Use it when the request needs API-style
options such as method, body, payload, file upload,
or an explicit destination with *into.
<serc-rod data='{"form":{"email":"","message":""},"result":null}'>
<input type="email" *input="form.email">
<textarea *input="form.message"></textarea>
<button
type="button"
*api="/api/contact.php"
method="POST"
body="form"
*into="result"
>Send</button>
<p *if="result" *print="result.message"></p>
</serc-rod>
In this example, *api sends form as the request body and stores the
response in result.
As a rule of thumb, use *fetch for simple JSON loading. Use *api
when the request behaves like an API operation and needs options beyond a simple JSON fetch.
Clickable elements and action timing
Some Sercrod directives change their behavior depending on whether they are attached to a clickable element.
This matters because the same directive can mean different timing depending on where it is placed. On a non-clickable element, it can behave as an initial setup or initial data loading instruction. On a clickable element, it becomes a user-triggered action.
In the current runtime, the following elements are treated as clickable triggers:
buttonawithout thedownloadattributeinput[type=button]input[type=submit]input[type=reset]
For *fetch, input[type=image] is also treated as clickable.
Non-clickable container
<serc-rod data='{"items":[]}' *fetch="/api/items.json:items">
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
Here, the request runs when the Sercrod element is rendered.
Clickable button
<serc-rod data='{"items":[]}'>
<button type="button" *fetch="/api/items.json:items">
Load items
</button>
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
Here, the request runs when the user clicks the button.
Clickable link
<serc-rod data='{"items":[]}'>
<a href="/items/" *fetch="/api/items.json:items">
Load items
</a>
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
An a element can be useful when the action should look like a link, or when it
belongs in a navigation-like UI. However, a also has normal link behavior through
href. If the request should be treated as an action rather than navigation,
that behavior must be designed intentionally.
For action-only behavior, button is usually clearer.
An a element with the download attribute is not treated as a
clickable request trigger in this rule. It is left for download behavior.
- Use non-clickable elements when the directive should run as part of initial rendering.
- Use clickable elements when the directive should run because the user performs an action.
- Use
buttonfor clear actions. - Use
aonly when link-like behavior is intentional.
Event attributes and update behavior
Sercrod event attributes use the event prefix defined in the runtime configuration.
By default, the prefix is @, so event handlers can be written as
@click, @input, @submit, and so on.
The prefix can be changed by configuration. For example, a project may use another prefix
such as ne- instead of @. The meaning is the same: the attribute
declares an event handler in the template.
Not every event should cause the template to update automatically.
Some events usually represent a user action that changes data, such as click,
input, change, submit, or drop.
These events are commonly used to update data or run an action.
Other events are mostly observation or movement events. Examples include
mousemove, pointermove, scroll,
touchmove, dragover, resize,
timeupdate, and selectionchange. These events can fire many
times in a short period. If Sercrod refreshed the template after every one of them,
the page could become heavy or unstable.
For that reason, Sercrod has a non_mutating event list. Events in this list are
treated as events that should not automatically trigger the normal template update flow.
The name non_mutating does not mean that the event can never change data.
It means that Sercrod does not treat that event as a normal update trigger by default.
This distinction is especially important for pointer, mouse, scroll, touch, and drag movement events.
For example, dragover is included in the non_mutating list because
it can fire continuously while the user drags over an element. However, drop
is not included. A drop event often represents the final action of a drag operation,
and it may need to update data or change the UI.
- Use event attributes such as
@clickor@submitfor actions that should change data or run a process. - Use high-frequency events such as
@mousemove,@pointermove,@scroll, or@dragovercarefully. - If a high-frequency event needs to update the UI, design that behavior explicitly instead of relying on automatic refresh after every event.
Configuration overview
Sercrod can be used with its default runtime settings in most pages.
Configuration becomes important when Sercrod is integrated into an existing site, an editor, an AI-assisted workflow, or another toolchain.
Configuration can affect syntax, events, filtering, auto definition, hooks, and integration behavior. For example:
- the event prefix, such as
@clickor another project-specific prefix - the
non_mutatingevent list - filters for text, HTML, URL-like attributes, and general attributes
- whether the custom element is defined automatically
- AST hooks and pre hooks used by tools or integrations
This page only gives an overview because configuration is cross-cutting. A full configuration reference should be maintained as its own page and should be based on the actual runtime source.
*for / *each
*for and *each both repeat data, but they do not produce the same
DOM structure.
*for repeats the element itself
<ul>
<li *for="post of posts">
%post.title%
</li>
</ul>
In this example, the li element is repeated for each item in posts.
<ul>
<li>First post</li>
<li>Second post</li>
<li>Third post</li>
</ul>
Use *for when each data item should create one repeated element, such as a list
item, card, row, or article.
*each keeps the container
<ul *each="post of posts">
<li>
%post.title%
</li>
</ul>
In this example, the ul remains a single container. The li inside it
is repeated for each item.
The simple rendered result may look similar to the *for example, but the meaning
is different.
- With
*for, the element that has the directive is repeated. - With
*each, the element that has the directive stays in place, and its children are repeated.
This difference matters when the element is a wrapper, a layout container, or a component boundary.
Choosing between *for and *each
<section *each="post of posts">
<article>
<h2>%post.title%</h2>
</article>
</section>
Use *each when the section should remain one container and only its
children should repeat.
<article *for="post of posts">
<h2>%post.title%</h2>
</article>
Use *for when each item should create its own article.
As a practical rule:
- Use
*forwhen the element itself represents one item. - Use
*eachwhen the element is a container and only its contents should repeat.
Also, do not place *each and *include or *import on the
same element. If a reusable part is needed inside a repeated area, put *include
or *import on a child element or wrapper inside the loop.
<div *each="post of posts">
<div *include="post-card"></div>
</div>
The important point is to decide what should be repeated: the element itself, or the children inside the element.
Sercrod-managed expression scope
Sercrod expressions are evaluated in a Sercrod-managed scope.
This is why a template can read naturally.
<serc-rod data='{"title":"Hello","posts":[{"title":"First"}]}'>
<h1 *print="title"></h1>
<ul>
<li *for="post of posts">
%post.title%
</li>
</ul>
</serc-rod>
In this example, title and posts come from the host data.
Inside *for, post is a loop-local variable created for each item.
Inside the repeated child nodes, %post.title% can read that local variable.
This is not ordinary JavaScript module scope. It is also not the same as writing every expression
as $data.title or $data.posts.
Sercrod creates an evaluation scope from the current data, local loop variables, parent and root data, event objects, the current element, internal helpers, and functions that are made available to the template.
Common values include:
$data$root$parent$event/$eel/$el- loop variables such as
postorindex - normal data paths such as
title,items, orform.email
Value expressions
<span *print="user.name"></span>
<p *if="user.active">Active</p>
<a :href="post.url">%post.title%</a>
Statement expressions
<button @click="count++">Count</button>
<button @click="send_message(message)">Send</button>
Text interpolation
<p>Hello %user.name%</p>
This scoped evaluation is what makes Sercrod templates compact. You can write
*print="title" or *for="post of posts" without turning every
expression into a long data access path.
At the same time, Sercrod expressions should be treated as Sercrod expressions, not as ordinary strict-mode JavaScript files. They are evaluated by the runtime in the context of the template.
Expressions should come from templates you control. Do not accept arbitrary external text and execute it as a Sercrod expression. External content should normally be treated as data, not as executable template logic.
Today, JavaScript does not provide a small, strict-mode-friendly standard mechanism for this kind of local expression scope. Sercrod keeps the runtime small and uses a practical runtime approach for now. If the JavaScript platform provides a safer standard scope mechanism in the future, Sercrod can move toward it without changing the idea of readable attribute-based templates.
Scope functions and external functions
Sercrod expressions can call functions as well as read values from Sercrod data.
A function does not have to be written directly inside an HTML attribute. When the logic becomes longer than a clear inline expression, define a small named function outside the template and call it from the attribute.
One way is to bring the function into the Sercrod expression scope with *methods.
In this document, we call this a scope function.
The following example intentionally defines an external label value first.
<script>
label = "Global label";
function format_label(value){
return `${this.label}: ${String(value || "").trim()}`;
}
</script>
<serc-rod
data='{"label":"Email","email":"USER@EXAMPLE.COM"}'
*methods="format_label"
>
<p *print="format_label(email)"></p>
</serc-rod>
In this example, format_label() is exposed through *methods.
The important difference is which this the function sees.
With *methods, format_label() is called as a scope function.
In this case, this refers to the Sercrod expression scope, so
this.label reads label from the Sercrod data.
The result is:
Email: USER@EXAMPLE.COM
Without *methods, the same function is used as an external function.
<script>
label = "Global label";
function format_label(value){
return `${this.label}: ${String(value || "").trim()}`;
}
</script>
<serc-rod data='{"label":"Email","email":"USER@EXAMPLE.COM"}'>
<p *print="format_label(email)"></p>
</serc-rod>
In this case, format_label() is not brought into the Sercrod expression scope.
The function call looks the same, but this is different. this no
longer refers to the Sercrod expression scope. It reads the external label value
instead.
The result is:
Global label: USER@EXAMPLE.COM
This shows the difference.
- With
*methods, the function reference is used as part of the Sercrod expression scope. - Without
*methods, the function is resolved as an external function, andthisfollows the external function call context. - Use a scope function when the function should work with the current Sercrod block's scope.
- Use an external function when the function is intentionally shared by the wider page or project and does not depend on the Sercrod scope.
In both cases, the attribute still shows where the function is called. The function body can stay outside the HTML.
Template AST hooks
Sercrod can expose a lightweight AST of the template structure.
This AST is not a JavaScript AST. It does not parse expressions such as
post.title or count++. It represents the HTML structure that
Sercrod reads from the template: elements, attributes, text nodes, comments, and children.
This is useful for tools that want to inspect a Sercrod template before or during rendering. For example, an AI tool, validator, editor, or documentation helper can look at the template structure and see which directives and bindings are present.
Inspecting the AST
<script>
Sercrod.register_ast_hook((ast, host) => {
console.log(ast);
});
</script>
<serc-rod data='{"title":"Hello"}'>
<h1 *print="title"></h1>
</serc-rod>
This first example only logs the AST so you can inspect the structure.
Using AST hooks for template checks
<script>
Sercrod.register_ast_hook((ast, host) => {
const walk = (node) => {
if(!node) return;
if(node.tag === "button" && node.attrs && (node.attrs["*api"] || node.attrs["*fetch"]) && !node.attrs.type){
node.attrs.type = "button";
}
if(Array.isArray(node.children)){
node.children.forEach(walk);
}
};
walk(ast);
});
</script>
<serc-rod data='{"result":null}'>
<button *api="/api/check.php" *into="result">Check</button>
<p *if="result" *print="result.message"></p>
</serc-rod>
This example shows the kind of small template support an AST hook can provide. The hook
inspects buttons that use *api or *fetch and ensures that they are
treated as action buttons.
AST hooks are extension points. They are not required for ordinary Sercrod templates. Most pages do not need to use them directly.
AST hooks and external tools
AST hooks can also connect Sercrod templates with external tools.
In the following example, the page already contains a real Sercrod form. The AST hook sends
the parsed template structure to an external analyzer. The analyzer reads the form structure
and returns a small JSON report. Sercrod stores that report in ast_report and
displays it like normal data.
<script>
Sercrod.register_ast_hook((ast, host) => {
if(host.dataset.ast_checked === "true") return;
host.dataset.ast_checked = "true";
fetch("/tools/sercrod-analyze.py", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
ast: ast
})
})
.then((response) => {
return response.json();
})
.then((report) => {
host.data = {
...host.data,
ast_report: report
};
});
});
</script>
<serc-rod data='{"form":{"email":"","message":""},"ast_report":null}'>
<input type="email" *input="form.email">
<textarea *input="form.message"></textarea>
<section *if="ast_report">
<h2>Template report</h2>
<p *print="ast_report.summary"></p>
<ul>
<li *for="field of ast_report.fields">%field%</li>
</ul>
</section>
</serc-rod>
A minimal external tool could return JSON like this:
{
"summary": "This template has 2 input binding(s).",
"fields": [
"form.email",
"form.message"
]
}
The external tool does not replace the Sercrod runtime. It reads the template structure and returns supporting data. Sercrod then renders that data in the same way it renders any other data.
This pattern can support documentation, validation, editor integration, AI-assisted analysis, or other project-specific tools without making the Sercrod runtime larger.
Frontend/server response contract
Sercrod attributes often define where a server response will be stored.
This means the frontend template and the server response shape must be designed together.
*api with *into
<serc-rod data='{"form":{"email":"","message":""},"result":null}'>
<input type="email" *input="form.email">
<textarea *input="form.message"></textarea>
<button
type="button"
*api="/api/contact.php"
method="POST"
body="form"
*into="result"
>Send</button>
<p *if="result" *print="result.message"></p>
</serc-rod>
In this example, the response from /api/contact.php is stored in
result.
Because the template reads result.message, the server should return an object
that has a message key.
{
"ok": true,
"message": "Submission received.",
"errors": [],
"received": {
"email": "user@example.com",
"message": "Hello"
}
}
The response root is part of the contract.
If the server suddenly returns a bare array, or renames message to another key,
the template will no longer match the response.
*fetch and URL:prop
<serc-rod data='{"items":[]}'>
<div *fetch="/api/items.json:items"></div>
<ul>
<li *for="item of items">%item.title%</li>
</ul>
</serc-rod>
Here, the fetched response is stored in items.
If the template uses *for="item of items", then items must be the
array that the template expects.
For list APIs, it is often safer to keep the response as an object and put the list under a named key.
{
"ok": true,
"items": [
{
"title": "First item"
},
{
"title": "Second item"
}
],
"count": 2
}
In that case, the frontend should match the shape.
<serc-rod data='{"posts":{"items":[],"count":0}}'>
<div *fetch="/api/items.json:posts"></div>
<p>Total: %posts.count%</p>
<ul>
<li *for="item of posts.items">%item.title%</li>
</ul>
</serc-rod>
The important rule is simple:
- Do not change the server response root without changing the Sercrod template.
- Do not change
*intoorURL:propwithout changing the data paths used by the template. - Keep the Sercrod template and the server response shape aligned.
A stable response shape makes the template easier to read, test, document, and generate by tools or AI.
Server sample files may be provided as readable references for this contract. In a real project, production endpoints should be implemented in a site-specific API location.