sercrod

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:

  1. where a value is written
  2. where the same value is read
  3. 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:

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.

*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:

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.

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.

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:

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.

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:

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:

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.

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:

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.