Svelte Core Best Practices
rilldata/rill
Rules for writing idiomatic Svelte 5 code: when to reach for runes like state, derived and effect, and how to handle props, attachments and bindings.
Documentation knowledge base for the ofa.js no-build front-end framework, with rules that keep generated code in ofa.js syntax rather than Vue or React habits.
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ofajs/ofa.js ofajs-docs --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/ofajs-docs-en .claude/skills/ofajs-docs && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .claude/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-enType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ofajs/ofa.js ofajs-docs --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/ofajs-docs-en .agents/skills/ofajs-docs && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .agents/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ofajs/ofa.js ofajs-docs --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/ofajs-docs-en .cursor/skills/ofajs-docs && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .cursor/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ofajs/ofa.js.git --path skills/ofajs-docs-en--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ofajs/ofa.js ofajs-docs --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/ofajs-docs-en .gemini/skills/ofajs-docs && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .gemini/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ofajs/ofa.js ofajs-docsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/ofajs-docs-en .github/skills/ofajs-docs && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .github/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ofajs/ofa.js --skill ofajs-docs -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ofajs/ofa.js ofajs-docs --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ofajs/ofa.js.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/ofajs-docs-en .opencode/skills/ofajs-docs && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "ofajs-docs" agent skill from https://github.com/ofajs/ofa.js/tree/main/skills/ofajs-docs-en into .opencode/skills/ofajs-docs/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ofajs-docs", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
ofajs-docsDocumentation knowledge base for the ofa.js no-build front-end framework, with rules that keep generated code in ofa.js syntax rather than Vue or React habits.
The skill tells the agent to rely on this documentation rather than outside ofa.js resources, to follow it when it conflicts with prior knowledge, and to keep every code example in the syntax described here. It forbids Vue, React or Angular conventions and the assumption that Node.js, Webpack or npm are needed, since ofa.js is a no-build MVVM framework. Other prohibitions include computed properties, routing parameters other than the query parameter in page modules, the same key in both attrs and data, and loading a page module directly through the o-app element, which only accepts app-config.js style config files.
A comparison table maps common mistakes to the right form: computed properties become getters in proto, v-if becomes the o-if component, v-for becomes o-fill with an optional fill-key, @click becomes on:click, dynamic classes use class:name, style binding uses a :style prefix, v-model becomes sync:value, and methods live in proto. Simple scalar values go in attrs while arrays and objects go in data, data is an object rather than a function, and inside o-fill the item is read through $data.
The skill carries many reference pages and numbered demo projects, from a first page and a switch to a todo list, a file list and routing. The excerpt is cut off after the comparison table.
3 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 8f95482. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (JavaScript, from the files we listed), which the agent can run.
Shell commands in SKILL.md call:
npmFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npm, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Guide to the ofa.js Framework loads about 13k tokens when it runs, and up to ~77k if it reads all its reference files. Until then it costs about 62 tokens; SKILL.md has 4,716 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from ofajs/ofa.js at commit 8f95482, republished under its MIT licence (© ofajs). 4,716 words, ~13,231 tokens.
.claude/skills/ofajs-docs/SKILL.md (or your agent's skills folder). This skill also uses 80 other files; get the full folder from GitHub.computed to define computed properties (ofa.js uses get keyword)query parameter in page modulesattrs and data<o-app src="./page.html"> to load a page module directly; <o-app> only accepts app-config.js type config files| ❌ Wrong Way | ✅ Correct Way | Description |
|---|---|---|
computed: { double() {} } | proto: { get double() {} } | Computed properties defined with getter in proto |
this.$route.query.id | { query } parameter | Get query parameters through function parameter |
v-if="show" | <o-if :value="show"> | Use o-if component for conditional rendering |
v-for="item in list" | <o-fill :value="list" fill-key="id"> | Use o-fill component for list rendering; fill-key is optional, but always add it when items have a unique field (e.g. id) |
@click="handle" | on:click="handle" | Event binding uses on: prefix |
:class="{ active: isActive }" | class:active="isActive" | Dynamic class uses class: syntax |
style="width: {{val}}" | :style.width="val" | Inline style binding uses :style. prefix |
v-model="value" | sync:value="value" | Two-way binding uses sync: syntax |
props: { msg: String } | attrs: { msg: 'default' } | Simple scalars (string) use attrs; complex data (array/object) use data |
methods: { foo() {} } | proto: { foo() {} } | Methods are defined in proto object |
data() { return { count: 0 } } | data: { count: 0 } | data is an object not a function |
attrs and data same key | Keep unique | attrs and data keys cannot be duplicated |
{{item.text}} | {{$data.text}} | Must use $data to access data inside o-fill |
{{element.name}} | {{$data.name}} | Must use $data to access data inside o-fill |
{{row.price}} | {{$data.price}} | Must use $data to access data inside o-fill |
:class="item.type" | attr:type="$data.type" | Property binding must also use $data |
proto: { $formatBytes() {} } | proto: { formatBytes() {} } | Custom methods don't use $ prefix |
proto: { back() {} } / data: { back: "" } (colliding with a built-in reserved name) | Avoid back / goto / replace / pageAnime / pageIsReady / src and any method on $.fn for custom methods/fields | These names are already taken by ofa.js: back() / goto() / replace() are the page instance's built-in navigation methods (back() is equivalent to this.app.back()), src is the page address property, and the generic $.fn methods (on / emit / $ / text, etc.) are unavailable too. On collision, newer versions throw a registration error like "'back' on 'proto' is already taken" and the whole page fails to register; a data field collision throws directly. See the detailed example below |
title="{{name}}" / :title="name" | attr:title="name" | {{...}} in attribute values is NOT parsed; dynamic attributes must use attr: |
attr:style="width: {{pct}}%" | :style.width="pct + '%'" | {{...}} is NOT parsed in attribute values; dynamic styles use :style. |
:disabled="isLoading" (boolean attributes like disabled/checked/readonly) | attr:disabled="isLoading" | :prop renders false as the attribute string "false"; HTML boolean attributes take effect whenever present, so the button stays disabled forever; attr: cancels the attribute setting entirely when the value is false |
| ❌ Wrong Way | ✅ Correct Way | Description |
|---|---|---|
.click(handler) | .on("click", handler) | Event binding uses .on() method |
.hide() .show() | .style.display = "none" / "" | No jQuery-style show/hide methods |
.html("xxx") .text("xxx") | .html = "xxx" .text = "xxx" | Set properties directly, not call methods |
ofaElement.addEventListener() | ofaElement.on() | ofa.js objects use on() method |
this.shadow.getElementById("id") | this.shadow.$("#id") | shadow is an ofa.js object, use $() method |
this.shadow.querySelector(".class") | this.shadow.$(".class") | Use $() method to select elements |
ofaElement.scrollTop etc. | ofaElement.ele.scrollTop | ofa.js objects access native properties via .ele |
document.querySelector("#id") | $("#id") | Use $() to get element instances globally; document.querySelector returns native elements lacking ofa.js enhanced methods and reactive features |
document.querySelector("o-app").goto(...) | $("o-app").goto(...) or this.app.goto(...) | Navigation methods like goto()/replace() only exist on $() wrapper objects, not on native DOM elements; inside page modules use this.app.goto(...) |
$("o-app").current.shadowRoot | $("o-app").current.ele.shadowRoot | $("o-app").current also returns an ofa.js wrapper object; native properties (shadowRoot, querySelector, etc.) must go through .ele, while ofa.js own properties (.src, .data, .app) can be accessed directly |
get xxx() { return this.obj.field } + template {{xxx}} (depends on async data) | Predefine xxx: "" in data, assign in ready/async callback | Getters are evaluated during module init (before ready()); if dependent data fields aren't assigned yet (especially null/undefined chained access), a TypeError will crash the entire page render. Getters are only suitable for simple computations depending on sync existing data (with initial values) |
Template expression referencing an undeclared variable ({{flag}} / :value="flag" / class:active="flag"...) | Declare every referenced key in data / attrs first (with safe defaults) | An undeclared key is NOT undefined — initialization throws Error evaluating element expression ... ReferenceError: flag is not defined and the whole page render is interrupted; commonly happens when adding new bindings to a template without syncing data |
Writing && inside an o-fill text interpolation (e.g. {{ $data.a && $data.b ? ... : '' }}) | Extract a $host.xxx($data) method; or rewrite with nested ternaries / === / !== | Compiling the o-fill {{}} expression with && throws SyntaxError: Unexpected token '&', and the entire o-fill block stops rendering (all list items disappear while the rest of the page stays fine); only a console error, the page itself is not interrupted |
| ❌ Wrong Way | ✅ Correct Way | Description |
|---|---|---|
<script> outside <template> | <script> inside <template> | script must be placed inside template tag |
export default async () => ({...}) | export default async ({ query }) => ({...}) | Page module should use parameter form to receive query |
<o-fill><template><div>...</div></template></o-fill> | <o-fill><div>...</div></o-fill> | Direct rendering doesn't need template wrapper |
<template> inside o-fill | <template> outside o-fill + name attribute | Template rendering requires template outside with name attribute |
<o-app src="./page.html?key=val"> to embed a sub-page inside a page | <o-page src="./page.html?key=val"> | Embed a page module with <o-page>; <o-app> is for micro-apps with app-config.js |
Use autoInstall in HTML | Use auto-install in HTML | Component attrs use camelCase in definitions, but must be converted to kebab-case (hyphenated) when used in HTML |
location.origin + location.pathname + "#./pages/x.html" | location.origin + "/#/pages/x.html" | Hash routing format is #/pages/xxx.html (a / directly after #, no ./ prefix, no extra pathname); use location.origin + "/#/..." when building external share links |
Setting src on <o-page> after initialization (including :src="url" dynamic binding, changing query to pass params) | Keep <o-page> resident + host calls a method exposed by the sub-page to pass params | The src of <o-page> is immutable after initialization; assigning it again throws A page that has already been initialized cannot be set with the src attribute; to destroy and recreate, wrap with o-if to toggle |
{{...}} Scope (Important){{expr}} only works in element text content. Writing it in HTML attribute values will NOT be parsed - the browser treats the entire curly braces as a static string.
❌ Wrong Way (using {{}} in attribute values):
<span title="{{$data.appId}}">{{$data.appId}}</span>
<a href="{{url}}">Link</a>
<img alt="{{name}}" src="/x.png">
<div data-id="{{id}}"></div>✅ Correct Way (attributes always use attr: / :prop / class: / :style.):
<span attr:title="$data.appId">{{$data.appId}}</span>
<a attr:href="url">Link</a>
<img attr:alt="name" src="/x.png">
<div attr:data-id="id"></div>Memory rule: {{}} only goes between >...<; all dynamic values inside the angle brackets use attr: / :prop / class: / :style. directives.
Why can't {{}} work in attribute values?
{{}} in attribute values>...<) are correctly parsed and reactively updated by ofa.jsattr: (Important)HTML boolean attributes like disabled / checked / readonly / hidden / open are presence-based — the attribute value doesn't matter; as long as the attribute exists, it takes effect. When binding a boolean state to such attributes, you must use attr:, not :prop.
❌ Wrong Way (:prop renders false as the attribute string "false", the attribute still exists, so the button stays disabled forever):
<p-button color="primary" :disabled="analyzing">Analyze</p-button>
<!-- When analyzing === false, it renders disabled="false" — still disabled! -->✅ Correct Way (the attr: rendering syntax removes the attribute setting entirely when it sees false):
<p-button color="primary" attr:disabled="analyzing">Analyze</p-button>
<!-- analyzing === false → no disabled attribute; analyzing === true → attribute present, disabled -->Why does :prop bite?
:prop-bound false gets serialized into the string "false" and lands on the attributedisabled="false" and disabled="true" count as present (disabled)attr: directive treats false specially: it removes the attribute entirely — no attribute means enabled againScope: all "present = on, absent = off" native boolean attributes, as well as boolean component properties defined via attrs and forwarded inside the shadow template with attr:xxx="xxx" (e.g. the disabled of punch-ui's p-button).
Data inherent properties (like type, status, level) should use attr: + attribute selector; style state switching (like active, disabled) should use class: + class selector.
❌ Wrong Way (using data property as class name):
<div class="message" :class="$data.type">
{{$data.text}}
</div>
<style>
.message.sent { color: blue; }
.message.received { color: green; }
</style>✅ Correct Way (using attribute binding):
<div class="message" attr:type="$data.type">
{{$data.text}}
</div>
<style>
.message[type="sent"] { color: blue; }
.message[type="received"] { color: green; }
</style>Why is this better?
type is a property of message type, not a style classElements obtained via $() are ofa.js wrapper objects with enhanced methods and reactive features; access native DOM elements via the .ele property.
Shadow object selector methods: this.shadow returns an ofa.js instantiated object, not a native ShadowRoot.
❌ Wrong Way (using native API):
const messagesDiv = this.shadow.getElementById("messages");
const element = this.shadow.querySelector(".class");✅ Correct Way (using ofa.js API):
const messagesDiv = this.shadow.$("#messages");
const element = this.shadow.$(".class");Native DOM property access: element.$() returns an ofa.js wrapper object; native properties need to be accessed via .ele.
❌ Wrong Way (operating directly on ofa.js object):
const messagesDiv = this.shadow.$("#messages");
messagesDiv.scrollTop = messagesDiv.scrollHeight; // scrollTop is a native property✅ Correct Way (accessing native properties via .ele):
const messagesDiv = this.shadow.$("#messages");
messagesDiv.ele.scrollTop = messagesDiv.ele.scrollHeight;Use cases:
.on(), .text, .html, etc.).ele (e.g., .scrollTop, .scrollHeight, .clientWidth, etc.)Common pitfall in Playwright tests / browser console: $("o-app").current also returns an ofa.js wrapper object, NOT a native DOM element. Accessing shadow DOM is easy to get wrong here.
❌ Wrong Way (accessing native properties directly on wrapper object):
// In Playwright tests or browser console
const cur = $("o-app").current;
cur.shadowRoot // → undefined (shadowRoot is a native property)
cur.shadowRoot.querySelector(...) // → throws "not a function"✅ Correct Way (go through .ele for native properties; ofa.js own properties can be accessed directly):
const cur = $("o-app").current;
cur.ele.shadowRoot // ✅ Access native shadowRoot via .ele
cur.ele.shadowRoot.querySelector(".item") // ✅ Native query
cur.src // ✅ ofa.js wrapper properties can be accessed directly
cur.data // ✅ ofa.js data can be accessed directly| Scenario | ❌ Wrong Way | ✅ Correct Way |
|---|---|---|
| Get current page's shadow DOM in Playwright/browser | $("o-app").current.shadowRoot | $("o-app").current.ele.shadowRoot |
| Query elements inside current page in tests | $("o-app").current.shadowRoot.querySelector(...) | $("o-app").current.ele.shadowRoot.querySelector(...) |
Memory rule: $() returns an ofa.js wrapper object, and .current is also a wrapper object. ofa.js own properties (.src/.data/.app) are used directly; browser-native properties and methods (.shadowRoot/.querySelector/.scrollTop) must always go through .ele.
$ is a reserved prefix for ofa.js built-in special variables ($data, $index, $host, $event). Custom proto methods must NOT use the $ prefix.
❌ Wrong Way (method name with $ prefix):
export default async () => {
return {
tag: "my-component",
data: { size: 1024 },
proto: {
$formatBytes(val) {
return (val / 1024).toFixed(2) + " KB";
}
}
};
};<span>{{$formatBytes(size)}}</span>✅ Correct Way (direct naming without prefix):
export default async () => {
return {
tag: "my-component",
data: { size: 1024 },
proto: {
formatBytes(val) {
return (val / 1024).toFixed(2) + " KB";
}
}
};
};<span>{{formatBytes(size)}}</span>Calling via $host in o-fill also without $:
<o-fill :value="files" fill-key="id">
<span>{{$host.formatBytes($data.size)}}</span>
</o-fill>The proto methods and data fields of a page module must not use names already occupied by ofa.js, otherwise module registration fails outright (the whole page cannot render). Newer versions log:
Page http://.../xxx.html has invalid registration parameters: 'back' on 'proto' is already taken, please rename 'back' to something else.Reserved built-in names (provided by the page instance):
back() (go back; equivalent to this.app.back()), goto(), replace()src (page address), pageAnime (transition animation), pageIsReady$.fn methods (on / off / emit / $ / text / html / css / data, etc.)A data field colliding with a reserved name throws directly (page_invalid_key); a proto method collision throws a registration error in newer versions, while older versions only console.warn — but the method still gets overwritten by the built-in implementation, so behavior is equally unreliable.
❌ Wrong Way (custom back collides with the built-in back-navigation method):
export default async () => ({
data: { dialogOpen: false },
proto: {
back() { // ❌ Collides with built-in back() navigation
this.phase = "input";
},
},
});✅ Correct Way (use a non-colliding, semantic name):
export default async () => ({
data: { dialogOpen: false },
proto: {
backToInput() { // ✅ Semantic name avoids collision with built-in back()
this.phase = "input";
},
},
});Debugging mnemonic: when the error says "'xxx' on 'proto' is already taken", that name is a built-in reserved name. First avoid back / goto / replace / src / pageAnime / pageIsReady and the generic methods on $.fn (see the built-in proto definitions in packages/ofa/page.mjs); prefer business-semantic names for custom methods (e.g. openXxx / saveXxx / backToInput).
{{...}} is NOT parsed in attribute values. For dynamic values:
attr:attributeName="expression":propertyName="expression" / sync:propertyName="expression"class:className="booleanExpression":style.propertyName="expression"❌ Wrong Way (using {{}} in attribute value, will NOT be parsed):
<div attr:style="width: {{pct}}%"></div>✅ Correct Way (using :style. to bind individual style property):
<div :style.width="pct + '%'"></div>Why is this better?
{{...}} in attribute values is NOT parsed, must use directive binding:style. value is a JavaScript expression, can freely concatenate stringsWhen using get xxx() {} to define a computed property for template {{xxx}} in a page module, the getter is evaluated immediately during module initialization, before ready() runs. If the getter accesses an object/array field in this.data that hasn't been initialized yet (especially null/undefined or deep chained access), a TypeError will be thrown and the entire page render will crash.
Typical error:
Error: Error evaluating text expression: 'roleText'❌ Wrong Way (getter depends on asynchronously fetched data):
export default async ({ query }) => {
return {
data: {
userInfo: {}, // Initially an empty object
},
get roleText() {
// Evaluated immediately at template init, userInfo is still {}
// Throws TypeError if userInfo is null/undefined or via deep chained access
return ROLE_TEXT[this.userInfo.role] || "";
},
ready() {
this.loadInfo(); // Asynchronously assigns userInfo, but too late
},
proto: {
async loadInfo() { /* ... */ }
}
};
};
// Template: {{roleText}}✅ Correct Way (predefine a safe default in data, assign in async callback):
export default async ({ query }) => {
return {
data: {
userInfo: {},
roleText: "", // Predefine a safe default value
},
ready() {
this.loadInfo();
},
proto: {
async loadInfo() {
const info = await api.getInfo();
this.userInfo = info;
this.roleText = ROLE_TEXT[info.role] || info.role || ""; // Assign in async callback
},
},
};
};
// Template: {{roleText}}Getter applicability boundaries:
get double() { return this.count * 2 } (count has initial value 0)Why is the getter evaluated immediately?
{{xxx}} expressions in the template during module initialization and establishes reactive dependenciesthis.xxx inside the getter and establishing dependency trackingready() only runs after initialization completes; async data hasn't arrived yetAll template expressions ({{xxx}}, :prop, sync:, class:, :style., attr:) are evaluated immediately during module initialization, and every key they reference must already be declared in data / attrs. Referencing an undeclared variable does NOT yield undefined — it throws directly and interrupts the whole page render:
Error: Error evaluating element expression: ':value="flag"', from file: ...
Caused by: ReferenceError: flag is not definedTypical scenario: adding a new feature to an existing page — the template gets new bindings but you forget to add the field to data. The error appears on first render, and the entire page/component render fails.
❌ Wrong Way (template uses noBg, not declared in data):
<x-if :value="noBg === 'off'">...</x-if>
<p-switch sync:value="noBg">No background</p-switch>
<script>
export default async () => ({
data: { dialogOpen: false }, // ❌ noBg declaration missing
});
</script>✅ Correct Way (declare it in data with a safe default):
<script>
export default async () => ({
data: { dialogOpen: false, noBg: "off" }, // ✅ every template-referenced key is declared
});
</script>Debugging mnemonic: Error evaluating element/class/... expression + ReferenceError: xxx is not defined → a template expression references a key that doesn't exist in data / attrs. Grep the template for xxx bindings first, then add the declaration to data.
Difference from the getter pitfall: the getter pitfall is a field declared but its value not arrived yet (throws TypeError); this pitfall is a field never declared at all (throws ReferenceError) — the latter is the easiest to hit when editing templates.
When building external share links (invitation links, email links, etc.) or using URLs to navigate directly in tests, the hash format is easy to get wrong.
ofa.js hash routing format: #/pages/xxx.html (a / directly after #, no ./ prefix).
❌ Wrong Way (extra pathname and ./ prefix):
const link = location.origin + location.pathname + "#./pages/set-password.html?token=xxx";
// Result: http://host/index.html#./pages/set-password.html?token=xxx ← Wrong✅ Correct Way (/ directly after #, no pathname):
const link = location.origin + "/#/pages/set-password.html?token=xxx";
// Result: http://host/#/pages/set-password.html?token=xxx ← CorrectMemory rule: A / immediately follows #, then the path starting from pages. For external share links, use location.origin + "/#/...".
When a single page module accumulates too much business (main list + dialog forms + multiple sub-flows), split independent business units (especially dialog forms) into separate page modules. The host embeds them with a resident <o-page>, communicating via "method call to pass params down + event bubbling to pass results up":
openForm(params)) to pass paramsthis.emit("xxx-save", { data, bubbles: true, composed: true }); the host listens with on:xxx-save on the <o-page> tag and reads from event.datacomposed: true is mandatory: the sub-page lives inside the host's Shadow DOM; when left as the default false, the event cannot cross the boundary and the host won't hear it❌ Wrong Way (changing src after initialization to switch params — throws at runtime):
<o-page :src="'./form.html?id=' + editingId"></o-page>The src of <o-page> is immutable after initialization; the source code throws directly on reassignment: A page that has already been initialized cannot be set with the src attribute.
✅ Correct Way:
<!-- Host page -->
<template page>
<o-page id="form-page" src="./form.html" on:form-save="onSave"></o-page>
<script>
export default async () => ({
proto: {
openForm(item) {
// $() returns an ofa.js wrapper object; call the sub-page method directly
this.shadow.$("#form-page")?.openForm(item);
},
onSave(event) {
console.log(event.data); // form values emitted by the sub-page
},
},
});
</script>
</template><!-- Sub-page form.html: carries its own p-dialog, exposes openForm for the host -->
<template page>
<p-dialog sync:open="dialogOpen" auto-close><!-- form controls sync:value="form.xxx" --></p-dialog>
<script>
export default async () => ({
data: { dialogOpen: false, form: {} },
proto: {
openForm(params) {
Object.assign(this.form, params); // fill in params
this.dialogOpen = true;
},
save() {
if (!this.form.name.trim()) return; // sub-page only validates non-empty
this.emit("form-save", {
data: { ...this.form },
bubbles: true,
composed: true, // cross Shadow DOM so the host can hear it
});
this.dialogOpen = false;
},
},
});
</script>
</template>Division of responsibility: the sub-page only handles form completeness and UI state; business normalization, id generation, and persistence belong to the host. Cancel/backdrop close only flips the sub-page's own dialogOpen and does not notify the host.
When a fresh instance is needed each time: if the sub-page is allowed to lose state, wrap <o-page> with o-if — closing destroys it, reopening recreates it (o-if toggling clears and re-renders its children); after reopening, params must be passed again via method call.
When to split:
data is polluted with lots of temporary state unrelated to the main content (form / dialogOpen / editingId …) → splitThe values of attr: / :prop / sync: / class: / :style. / on: are always parsed as JavaScript expressions. You cannot write bare identifiers or bare string literals. Strings must be quoted; JS reserved words (in / class / for, etc.) on their own are illegal as expressions and throw a SyntaxError immediately.
Typical error (console keeps logging the error, some page functionality breaks):
SyntaxError: Unexpected token 'in'❌ Wrong Way (writing attr:data-type="in" as a plain attribute value with a bare literal — in is a JS reserved word parsed as an expression):
<button attr:data-type="in">Stock in</button>
<!-- ofa.js treats the value "in" as an expression → SyntaxError: Unexpected token 'in' -->✅ Correct Way (method name / string literal inside expression):
<button on:click="$host.stockIn($event)">Stock in</button>
<!-- Bind the event to a method name to avoid writing bare literals in directive values -->
<button attr:data-type="'in'">Stock in</button>
<!-- When you really need to pass a literal, quote it as a string expression -->Debugging mnemonic: SyntaxError: Unexpected token '<xxx>' (words like in/for/if) → a bare identifier was written in a directive attribute value. Prefer refactoring "type-identifying" scenarios into method dispatch (e.g. on:click="$host.stockIn($event)"), and quote string literals when placing them in attr: values (attr:data-type="'in'").
Addendum: never use attr: for static values — a bare static string throws (ReferenceError or SyntaxError) and aborts the component render — the rule above only covers reserved words (a syntax error). The more common and far more insidious case is putting a plain static string straight into attr:: the value is not a reserved word, so it is evaluated as an identifier and throws at runtime:
❌ <button attr:title="pending"> <!-- one bare identifier → ReferenceError: pending is not defined -->
❌ <button attr:title="点这里选择订单状态"> <!-- localized text is usually ONE valid identifier → ReferenceError (the classic real-world trap) -->
❌ <button attr:title="Choose a status"> <!-- several bare words → SyntaxError: Unexpected identifier -->
✅ <button title="点这里选择订单状态"> <!-- static value → plain attribute (ofa ignores attributes without a directive prefix) -->
✅ <button attr:title="'点这里选择订单状态'"> <!-- if you must use attr:, wrap it as a string expression -->
✅ <button attr:title="locked ? 'Locked' : ''"> <!-- keep attr: for real dynamic expressions -->⚠️ The real trap is the knock-on symptom: that exception aborts the whole component/page render, while the template HTML has already been written into the shadowRoot. What you observe is therefore "the component looks alive (its DOM is there), but ready() never runs, every later o-fill / o-if stays unexpanded, and the data looks empty" — which is extremely easy to misdiagnose as "the property binding doesn't work / the API returned nothing / the component was never upgraded". (Real case: a filter component with a mistyped attr:title="…" showed only the one static item in its dialog; changing it back to a plain title="…" made ready() run immediately and the whole list render.)
Debugging mnemonic: attr:xxx="static text" → ReferenceError: xxx is not defined (single word / localized text) or SyntaxError: Unexpected identifier (several words); when the symptom is "template present, but ready() never ran and o-fill is not expanded", first grep every attr: value for bare text that is neither an expression nor a data/$data field name (non-ASCII characters are the clearest signal). To confirm whether ready() actually ran, set a marker on window inside it and read it back (if the project has a component that intercepts console — e.g. a log-show overlay — console.log output will be invisible).
ofa.js has an in-memory module cache for already-loaded page modules (reusing component/page module definitions for the same URL), and pages pull their template files via fetch — if the static server sends HTTP caching headers (e.g. http-server without -c-1), the browser also hits the disk cache. The combined effect of both caches: you change the page file, but hash navigation (without a full page reload) still renders the old version, with no errors in the console — extremely easy to misdiagnose as "my code is wrong" and waste time debugging.
Typical scenario: in Playwright tests or a browser, navigating directly to #/pages/xxx.html for debugging; after repeatedly modifying the page template, the effect never changes. Even corrupting the file into an obviously broken version still renders the old logic normally.
✅ Correct approach:
http-server . -p 5173 -c-1 (-c-1 = disable cache; npm run dev already includes it, npm start does not).http://localhost:5173/index.html?t=v1#/pages/xxx.html — when the query changes, fetch treats it as a new URL and bypasses the cache.page.goto(url) to load the whole page directly.Debugging mnemonic: code changes not taking effect + no console errors → suspect caching first (module cache / HTTP cache); force-refresh with a query-bearing URL to rule it out. Don't bisect your own code first.
$host / $data Are Only Available in o-fill's item Scope — Use Method Names Directly at Root Level (Important)$host / $data are injected by ofa.js into the item scope when x-fill (o-fill) renders list items (createItem creates { $data, $host, $index }). The root-level scope (top of the page template, outside o-fill) has no $host / $data — on:click="$host.xxx()" throws Error evaluating element expression: 'on:click="$host.xxx()"'; clicks do nothing and the console logs an error.
✅ Correct Way:
<!-- Root level: write the method name directly (proto methods live on the page instance) -->
<button on:click="openStockHelp()">?</button>
<button on:click="goToPage(currentPage - 1)">Previous</button><!-- Inside o-fill: $data / $host / $index are available -->
<o-fill :value="rows" fill-key="id">
<button on:click="$host.deleteRow($data.id)">{{$data.name}}</button>
</o-fill>❌ Wrong Way (using $host at root level):
<button on:click="$host.openStockHelp()">?</button> <!-- throws -->Debugging mnemonic: an event expression like on:click reports Error evaluating element expression → first check whether the element is inside an o-fill. If it isn't, drop the $host. and write the method name directly (keep $host only for things like numeric page buttons inside an o-fill). For property bindings (:disabled="page <= 1") at root level, use the data field name directly — no $host needed.
Addition (the property-binding channel hits the same trap): $host.xxx references in root-level (outside o-fill) o-if :value and attr: bindings silently fail — no error, no exception; the content / attribute is simply never rendered (the o-if never shows, the attr is never set). For example:
<!-- ❌ Root-level o-if content never renders (even when the condition is true) -->
<o-if :value="!$host.warehouseId">Please select a warehouse</o-if>
<!-- ❌ Root-level attr: is never set (input always enabled) -->
<input attr:disabled="!$host.canInput" />✅ Correct way: in property bindings use data field names directly (o-if :value="warehouseId === ''" / attr:disabled="!warehouseId || !selectedChannel"); also don't bind attr: to a proto getter (!$host.canInput doesn't render) — expand the condition into an expression over reactive data fields. Debugging mnemonic: root-level o-if content missing / attr not applied with no console error → check whether the binding expression references $host (root level has no $host; it is only injected into o-fill's item scope).
&& in an o-fill text interpolation (block stops rendering, important)Symptom: after adding a && expression to a text interpolation inside an o-fill (e.g. {{ $data.x && $data.x !== '裸果' ? ' · 内包装 ' + $data.x : '' }}), the entire o-fill block stops rendering (all list items disappear), while other parts of the page (titles / toolbars / pagination) still work. There is no page-level error — only a console SyntaxError: Unexpected token '&' thrown when compiling with new Function.
Minimal repro comparison (the {{}} text-interpolation channel):
{{ $data.pack && $data.pack ? ... : '' }} (contains &&) → ❌ the whole o-fill does not render{{ $data.pack ? '· ' + $data.pack : '' }} (ternary + concat) → ✅{{ $data.pack === '裸果' ? '' : ... }} (===) → ✅{{ $data.pack !== '裸果' ? ... }} (!==) → ✅{{$host.xxx($data)}} (method call) → ✅Root cause: the o-fill item template encodes the {{}} expression with encodeURIComponent into the expr attribute and decodes it back before compiling; && gets corrupted in this pipeline (a lone & survives), so new Function fails to parse it. The failure happens inside that o-fill's render loop, which aborts the whole block. !==, ===, ternaries and string concatenation are all unaffected.
Fix: avoid && in text interpolations and extract a $host method instead (regular JS inside methods is not subject to template compilation):
// in proto
innerPackingText(d) {
const ip = d && d.inner_packing;
if (!ip || ip === "裸果") return "";
return " · 内包装 " + ip;
}<!-- in template -->
<div>{{$host.innerPackingText($data)}}</div>Debugging mnemonic: o-fill block not rendering + console shows SyntaxError: Unexpected token '&' → grep that o-fill for && inside {{ expressions and convert them all to method calls. (Whether && is safe in the property-binding channel is unverified — when in doubt, methodize rather than gamble.)
Status: an ofa.js module-compilation implementation defect (
drawUrlsplit<script>on;before rewriting imports); fixed in newer versions. Placing comments (//,/* */, trailing comments) between import declarations is standard, legal ESM and now works without any workaround. If you hitFailed to resolve module specifier "../.."on old versions (≈v4.7.2 and earlier) with the error "moving" from one import to the next, upgrade the framework; the temporary workaround was to keep comments after the import block.
<template page> contains <style>, template content, and <script>, script must be inside template<template component> contains <style>, template content, and <script>, script must be inside template, returned object must include tag field| Tag | Purpose | src Points To |
|---|---|---|
<o-page> | Embed a page module in an entry HTML or inside another page template | Directly to a page module file (.html) |
<o-app> | Create a micro-app that manages multi-page navigation and transitions | An app config file (app-config.js) |
Key differences:
<o-page> is a "page-level component" that loads and renders a page module. It can be used in the entry HTML or inside another page's template to embed a sub-page.<o-app> is a "micro-app container" for creating independent application instances. It loads app-config.js to configure the home page and page transition animations. Do not use <o-app> to directly load page module files.Embedding a sub-page example (embedding a page module inside another page's template):
<template page>
<p-dialog>
<o-page src="./user-traffic-page.html?userId=123"></o-page>
</p-dialog>
<script>
export default async () => {
return {
data: { ... }
};
};
</script>
</template>The sub-page receives the userId parameter via export default async ({ query }).
⚠️ The
srcof<o-page>(including query) only takes effect at initialization; assigning it again after initialization throws an error. For runtime param passing, call a method exposed by the sub-page, and pass results back via event bubbling (bubbles+composed) — see the "Splitting a Complex Single Page into Multiple Page Modules" example above.
<template page>
<style>
:host { display: block; }
</style>
<div>{{message}}</div>
<script>
export default async ({ query }) => {
return {
data: { message: "Hello" },
proto: { handleClick() {} }
};
};
</script>
</template><template component>
<style>
:host { display: block; }
</style>
<div>{{value}}</div>
<script>
export default async () => {
return {
tag: "my-component",
attrs: { value: "default" },
data: { count: 0 },
proto: { increment() {} }
};
};
</script>
</template>
attrsvsdatanote:attrsis for simple scalar values (string). Its values reflect to HTML attributes, suitable forattr:xxxCSS selectors.datais for complex data (arrays, objects). When bound via:propfrom outside,attrsvalues get serialized to strings causing type loss, so complex data like arrays and objects must be placed indata. Keys inattrsanddatacannot overlap.
| Syntax | Purpose | Example |
|---|---|---|
{{var}} | Text node rendering (only in element content, NOT in attribute values) | <span>{{name}}</span> |
:html | HTML content rendering | <div :html="htmlContent"></div> |
:prop="key" | One-way property binding | <input :value="name"> |
sync:prop="key" | Two-way property binding | <input sync:value="name"> |
attr:name="key" | HTML attribute binding (title/href/alt/data- etc. always use this*) | <a attr:href="url" attr:title="tip"> |
class:name="bool" | Conditional class binding | <div class:active="isActive"> |
:style.prop="value" | Style property binding | <p :style.color="textColor"> |
on:event="handler" | Event binding | <button on:click="handleClick"> |
on:event="expr" | Expression event | <button on:click="count++"> |
$event | Event object | on:click="handle($event)" |
$("#id") | Get element instance | const el = $("#myComponent") |
get xxx() {} in proto instead of computed$.stanz()<o-fill> component; fill-key is optional, but always add fill-key="fieldName" when list items have a unique identifier field (e.g. id), so items are correctly reused and updated when the array is added to, removed from, or reordered<o-if> / <o-else-if> / <o-else> components<x-if> / <x-fill> have same functionality but don't render to DOM:toKey="fromKey" one-way, sync:toKey="fromKey" two-waywatch: { prop() {} }ready() attached() detached()this.emit('event-name', { data: {...} })<slot></slot> receives external contentNeed reusable components?
├─ Yes → Use component module (<template component> + tag field)
└─ No → Use page module (<template page>)
Is the single page too heavy (main list + dialog form + multiple sub-flows mixed in one module)?
├─ Yes → Split into multiple page modules: the host embeds sub-pages with a resident <o-page>
│ ├─ Pass params on first initialization: query in the src URL, e.g. src="./sub-page.html?userId=123"
│ ├─ Pass params at runtime: host calls a method exposed by the sub-page (src is immutable after initialization; never change src/query)
│ └─ Pass results back: sub-page emits a bubbling event (bubbles + composed), host listens with on:eventName
└─ No → Keep a single page moduleNeed to share data?
├─ Yes → Across multiple layers of components?
│ ├─ Yes → Use o-provider/o-consumer
│ └─ No → Use sync: two-way binding or : one-way passing
└─ No → Use data to define local dataWhen defining component properties, should the value go in attrs or data?
├─ Simple scalar values (string) → Use attrs
│ └─ Reflects to HTML attribute, usable with attr:xxx in CSS selectors
├─ Complex data (arrays, objects) → Use data
│ └─ When bound via :prop from outside, attrs serializes to string causing type loss
└─ Example: <n-line-chart :points="someArray"> → points is an array, must be in dataList rendering?
├─ Yes → Use o-fill component
│ ├─ Items have a unique field (e.g. id) → Add fill-key="id" (optional attribute, but always include it when writing code)
│ ├─ Direct rendering (simple structure) → Template content directly inside o-fill, no <template> wrapper needed
│ └─ Template rendering (complex structure/reuse) → <template> defined outside o-fill, use name attribute to bind
└─ No → Write template normally
Conditional rendering?
├─ Yes → Use o-if/o-else-if/o-else components
└─ No → Write template normallyo-fill Direct Rendering (recommended for simple structures):
<o-fill :value="messages" fill-key="id">
<div class="message" attr:type="$data.type">
[{{$data.time}}] {{$data.text}}
</div>
</o-fill>$data, $index, $host to access datafill-key when items have a unique field (e.g. id) — optional, but recommended to always includeo-fill Template Rendering (for complex structures or reuse):
<o-fill :value="products" name="product-template" fill-key="id"></o-fill>
<template name="product-template">
<div class="product-card">{{$data.name}} - ¥{{$data.price}}</div>
</template>Need to set styles based on data?
├─ Data inherent properties (like type, status, level) → Use attr: + attribute selector
└─ Style state switching (like active, disabled) → Use class: + class selectorNeed multi-page application?
├─ Yes → Use o-router + o-app
│ └─ Need nested layout?
│ ├─ Yes → Parent page uses <slot>, child page exports parent
│ └─ No → Independent page
└─ No → Single page application| Document | Description |
|---|---|
| Template Syntax Examples and Syntax Explanation | Complete examples and detailed explanations of all template syntax (Highest priority) |
| Quick Reference Table | API and syntax quick reference |
| API Reference Manual | Complete API documentation |
| Common Patterns and Best Practices | Common code patterns (including single-page business splitting / embedded sub-page pattern) |
| Document | Description |
|---|---|
| Introduction | Framework core concepts and advantages |
| Script Reference | Import methods |
| Quick Start | Quick start guide |
| Create First App | Create project using OFA Studio |
| Production and Deployment | Development environment, production deployment, minification |
| Quick Syntax | Document |
|---|---|
{{variable}} :html | Content Rendering |
on:click="handler" | Event Binding |
:prop="value" sync:prop="value" | Property Binding |
class:active="isActive" :style.width="val" | Class/Style Binding |
<o-if :value="condition"> | Conditional Rendering |
<o-fill :value="list" fill-key="id"> | List Rendering |
get computedProp() {} | Computed Properties |
watch: { prop() {} } | Watchers |
ready() attached() detached() | Lifecycle |
| Quick Syntax | Document |
|---|---|
<template component> tag attrs | Create Component |
export default async ({ load, url, query }) | Module Return Object Properties |
<slot></slot> | Slots |
this.emit('event') | Custom Events |
attrs: { msg: 'default' } | Inherit Attributes |
:toProp="fromProp" | Deep Property Binding |
{{obj.nested.prop}} | Property Response |
<inject-host> | Inject Host Style |
<x-if> <x-fill> | Non-explicit Component |
<template is="replace-temp"> | Replace Template |
<match-var> | Match Var |
| Quick Syntax | Document |
|---|---|
o-provider o-consumer | Context State |
$.stanz() | State Management |
o-app o-router | Routes |
Parent page <slot> child page parent | Nested Routes |
app-config.js | App Configuration |
o-app micro app | Micro App |
| SCSR isomorphic rendering | SSR and Isomorphic Rendering |
| Example | Feature Points | Entry | Key Files |
|---|---|---|---|
| Counter | Data binding, events, computed properties, styles | demo.html | page.html |
| Switch Component | Component definition, property passing, events, slots | demo.html | switch.html, page.html |
| Todo List | Data persistence, list rendering, state management | demo.html | page.html, data.js |
| File Editor | Nested component communication, o-provider, dependency injection | demo.html | page.html, filelist.html, editor.html |
| SPA Routing | o-router, o-app, page animation | demo.html | app-config.js, layout.html |
| SCSR Rendering | Server-side rendering, SEO, isomorphic application | home.html | app-config.js |
| Shadow DOM | shadow operations, component method definition | demo.html | shadow-demo.html |
© ofajs, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 80 other files (references, assets) in skills/ofajs-docs-en of ofajs/ofa.js.
Open the folder on GitHubat commit 8f95482
Guide to the ofa.js Framework next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Guide to the ofa.js Framework this skillofajs/ofa.js | 754 | — | ~13k | Automated safety check: Pass | MIT | |
| Svelte Core Best Practicesrilldata/rill | 2.9k | 4 repos | ~1.8k | Automated safety check: Pass | Apache-2.0 | |
| Frontend Patternskurealnum/dotfiles | 290 | 19 repos | ~3.7k | Automated safety check: Pass | None | |
| LobeHub Data Fetching Layerslobehub/lobehub | 83k | — | ~1.7k | Automated safety check: Pass | Custom licence | |
| Cursor BYOK Frontend Guideleookun/cursor-byok | 3.2k | — | ~1.9k | Automated safety check: Pass | MIT | |
| Oil Frontend Rulesoil-oil/oil-frontend | 132 | — | ~1.4k | Automated safety check: Pass | MIT |
rilldata/rill
Rules for writing idiomatic Svelte 5 code: when to reach for runes like state, derived and effect, and how to handle props, attachments and bindings.
kurealnum/dotfiles
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
lobehub/lobehub
Explains how LobeHub client code fetches data through services, SWR store hooks and cache keys, and when to avoid useEffect fetching or duplicated state.
leookun/cursor-byok
Sets the rules for building the Cursor BYOK desktop app's React and Tauri frontend, especially its HTTP boundary and component state architecture.
oil-oil/oil-frontend
Chinese-language rules for building, changing and reviewing product front ends, covering interface, interaction, state and data flow and component organization.
redis/RedisInsight
React/Redux frontend development patterns for RedisInsight UI: component folder structure, styled-components, hooks, named exports, barrel files, layout components, and theme usage.
ofajs/ofa.js
Reference knowledge base for the no-build ofa.js framework, with syntax rules and a table of mistakes to avoid when writing components, pages and routes.
Categories
Documentation knowledge base for the ofa.js no-build front-end framework, with rules that keep generated code in ofa.js syntax rather than Vue or React habits. js resources, to follow it when it conflicts with prior knowledge, and to keep every code example in the syntax described here.js is a no-build MVVM framework.
Guide to the ofa.js Framework fits situations like: building a web app with ofa.js without a build step; writing ofa.js components and page modules; configuring routing through an app-config.js file; translating Vue-style code into ofa.js syntax.
Run `npx skills add ofajs/ofa.js --skill ofajs-docs -a claude-code`. Or copy the skill folder (skills/ofajs-docs-en in ofajs/ofa.js) into .claude/skills/ofajs-docs in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ofajs/ofa.js --skill ofajs-docs -a codex`. Or copy the skill folder (skills/ofajs-docs-en in ofajs/ofa.js) into .agents/skills/ofajs-docs in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ofajs/ofa.js --skill ofajs-docs -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/ofajs-docs, .gemini/skills/ofajs-docs, .github/skills/ofajs-docs and .opencode/skills/ofajs-docs in your project.
Going by SKILL.md and its folder, Guide to the ofa.js Framework needs JavaScript for the scripts in its folder and the command-line tools its instructions call (npm).
SKILL.md contains no URLs. Its commands use npm, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Guide to the ofa.js Framework is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 13k tokens (SKILL.md is roughly 53k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 63k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Guide to the ofa.js Framework: Svelte Core Best Practices (rilldata/rill, 2.9k stars), Frontend Patterns (kurealnum/dotfiles, 290 stars), LobeHub Data Fetching Layers (lobehub/lobehub, 83k stars) and Cursor BYOK Frontend Guide (leookun/cursor-byok, 3.2k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ofajs (a GitHub organization) maintains it in ofajs/ofa.js, which has 754 GitHub stars. The repository holds 2 skills in this directory. The repository was last updated on October 6, 2026.
Source: ofajs/ofa.js on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.