Documentation

Core Concepts

How the RJS class (src/model/RJS.ts) parses template directives and renders the DOM in a single, non-reactive pass.

Instance Lifecycle

"#selector".RJS(body) looks up the target element via body.id, clones it as the pristine template (#model), and stores data/event/when on the instance. If the selector resolves to null, the constructor returns early and no instance is initialized.

graph TB
    Entry["#selector.RJS(body)"] --> Ctor["constructor: locate node, cloneNode(true)"]
    Ctor --> Init["#init()"]
    Init --> Before["when.before_render()"]
    Before -->|"returns false"| Cancel["render cancelled"]
    Before -->|"continue"| SetDOM["#SET_DOM(body, data) — recursive walk"]
    SetDOM --> After["when.rendered()"]

app.renew(body) restores the DOM from the cloned #model (replaceChildren(...this.#model.children)), merges any provided data/event/when keys into the existing instance state (unmentioned keys keep their prior value), and re-runs #init(). This is the only way an RJS instance re-renders — there is no automatic reactivity or dirty-checking.

Directive Parsing Order

#SET_DOM walks each child element and applies directives in a fixed order. :path- and :for-tagged nodes short-circuit into their own handlers and skip the rest of the pipeline for that node:

graph TB
    Node["child element"] --> PathCheck{":path attr?"}
    PathCheck -->|yes| SetPath["#setPath: fetch fragment, inline it, return"]
    PathCheck -->|no| ForCheck{":for attr?"}
    ForCheck -->|yes| FitFor["#FIT_FOR: expand loop, return"]
    ForCheck -->|no| SetIf["#setIf: :if / :else-if / :else"]
    SetIf --> FitModel["#FIT_MODEL: two-way form binding"]
    FitModel --> FitAttr["#FIT_ATTRIBUTE: :[attr] / :[css]"]
    FitAttr --> FitEvent["#FIT_EVENT: @[event]"]
    FitEvent --> FitText["#FIT_TEXT: {{ }} interpolation"]
    FitText --> Recurse["#SET_DOM(child) — recurse into grandchildren"]

Elements whose visibility is still pending (:else-if/:el-if/:else siblings not yet resolved by #setIf) are skipped on this pass; #setIf re-invokes #SET_DOM on the parent once it has removed the losing branches.

:path — async fragment loading

Applies only to elements carrying a :path attribute. #setPath fetches the target URL, injects the returned HTML as child nodes, and re-runs #SET_DOM on the fragment (unless the fragment contains a new PD construction, in which case it's left untouched). Script tags found in the fragment are moved to the end of document.body and executed; everything else is unwrapped in place, and the original placeholder node is removed. A fetch failure also removes the node.

:for — loop expansion

Supports three forms: item in items, (item, index) in items, and (key, value) in object. #FIT_FOR clones the template node once per array entry (or object key), binds subItem = { [val0]: item, [val1]: index } (or { [val0]: key, [val1]: value }), and renders each clone's attributes/events/text before inserting the batch back where the original node stood. Nested-object items get a full recursive #SET_DOM pass; primitive items only get attribute/event/text substitution.

:if / :else-if / :el-if / :else

#setIf collects the :if node and any immediately-following :else-if/:el-if/:else siblings into one group, evaluates each condition in order, and removes every node except the first one that matches (later matches in the group are removed even if their own condition is true). Supported operators: >, <, >=/>==, <=/<==, ==/===, !=/!==. ==/!= special-case the literals null, true, false, and empty (empty-string check) before falling back to string equality.

:model — two-way form binding

#FIT_MODEL inspects the bound element's type: checkbox/radio inputs listen on change and join all checked values with ,; select listens on change; plain input/textarea listen on both keyup and change. In every case the handler writes straight into the data object passed to that render pass — there is no observer or reactive re-render triggered by the write.

:[attr] / :[css] — attribute and style binding

#FIT_ATTRIBUTE collects every attribute matching /^:[\w\-]+$/ (excluding the directive names above), resolves its value through #GET_VALUE, and hands it to #SET_ATTRIBUTE. :class calls _class() (see API Reference); :id/:src/:alt/:href/:html map directly onto the matching DOM property; any other camel-cased key that exists on element.style is treated as a CSS property; everything else becomes a plain HTML attribute (or is removed if the resolved value is empty).

@[event] — event binding

#FIT_EVENT matches attributes like /^@[\w]+$/, rewrites @click to onclick, and looks up the handler name in this.event. Inside a :for loop, the handler name can itself be a per-item value (resolved through the same key-path lookup used for {{ }} interpolation) rather than a fixed method name.

{{ }} — text interpolation

#FIT_TEXT (top level) / #FIT_TEXT_IN_ARRAY / #FIT_TEXT_IN_OBJECT (inside :for) scan text nodes for {{ ... }} and resolve each match through #GET_VALUE, which supports:

Helper Behavior
{{ key.path }} Dot-path lookup into data, walked layer by layer via #GET_VALUE_FROM_LAYERS
{{ CALC(key + n) }} Arithmetic: + - * / %, applied via #CALC
{{ LENGTH(key) }} Object.keys(value).length for plain objects, value.length otherwise
{{ UPPER(key) }} / {{ LOWER(key) }} .toUpperCase() / .toLowerCase()
{{ DATE(key, format) }} Parses key as a Unix timestamp and formats it (see Number.$format in API Reference)

REGEX_TEXT also matches a value embedded inside surrounding literal text (prefix {{ key }} suffix), preserving the prefix/suffix when substituting.

Non-Reactivity Is Deliberate

Every commented-out block in RJS.ts (#dataListener, the Object.defineProperty getter/setter pairs) is a removed reactive-observer mechanism. The shipped design renders once per #init() call and requires an explicit renew() to update — this trade-off is what the README describes as removing "the overhead of automatic reactive watching."

中文