JS Framework
ES modules for XHR, reactive state, HTML templating, navigation, WebSockets, internationalisation and markdown rendering. No build step, no bundler — just import and use.
Module map
| Module | Path | Contents |
|---|---|---|
vesta.mjs | /static/framework/vesta.mjs | XHR, reactive state, utility functions |
navigation.mjs | /static/framework/navigation.mjs | Page navigation, menus, modals, steps |
websockets.mjs | /static/framework/websockets.mjs | WebSocket client with auto-reconnect |
global.mjs | /static/framework/global.mjs | Shared application state object |
utils.mjs | /static/framework/utils.mjs | Miscellaneous helpers |
markdown.mjs | /static/markdown/markdown.mjs | Custom markdown-to-HTML renderer |
translation.mjs | /static/translations/translation.mjs | i18n string loader |
Import what you need in your entry point (static/main.mjs):
import { xhr, fillWith, Subscribe, updateElement } from '/static/framework/vesta.mjs';
import { goTo, toggleMenu, toggleFM } from '/static/framework/navigation.mjs';
import G from '/static/framework/global.mjs';
HTTP requests — xhr()
XMLHttpRequest. Calls effect(responseText)
when the request completes. Use this instead of raw fetch to stay
consistent with the reactive system.
| Parameter | Type | Description |
|---|---|---|
endpoint | string | URL, e.g. "/api_posts?page=2" |
effect | function | Callback receiving the response string |
method | string | "GET" (default) or "POST" |
async | bool | Defaults to true |
body | string | null | Request body for POST |
// Simple GET
xhr('/api_posts', (raw) => {
const posts = JSON.parse(raw);
console.log(posts);
});
// POST with JSON body
xhr('/api_create', (raw) => {
const result = JSON.parse(raw);
if (result.ok) updateElement('postList', ...);
}, "POST", true, JSON.stringify({ title, content }));
Global state — global.mjs
A single plain object exported from global.mjs. Import it
anywhere — it's the same reference everywhere (ES module singleton).
import G from '/static/framework/global.mjs';
// Write
G.user = { id: 42, name: "Alice" };
G.posts = [];
// Read anywhere
console.log(G.user.name);
Typical top-level shape used with Subscribe:
// global.mjs — initialise your state shape here
const G = {
user: null,
posts: [],
settings: {},
i18n: {},
};
export default G;
Reactive rendering
Subscribe
updateElement / setElement etc., the element re-renders.
| Parameter | Description |
|---|---|
element | CSS selector string targeting the DOM element |
content | Function returning an HTML string. Receives the current state value. Escape any untrusted data with escapeHTML() (see Output escaping). |
className | Name of the state variable in G (or any namespace) |
params | Options: {target: "html"|"style"|"class", element: stateVar, repaint: fn} |
import G from '/static/framework/global.mjs';
import { Subscribe, updateElement } from '/static/framework/vesta.mjs';
// Bind #post-list to G.posts — escapeHTML() guards the untrusted title against XSS
Subscribe('#post-list', (posts) => {
return posts.map(p => `<div class="post">${escapeHTML(p.title)}</div>`).join('');
}, 'posts', { target: 'html', element: G.posts });
// Later — update state and re-render automatically
G.posts = [{ title: "Hello" }, { title: "World" }];
updateElement('posts', G.posts);
State mutation helpers
value to state key and re-render all subscribers of that key.value to an array state variable and re-render.value into an object state variable and re-render.index from an array state variable and re-render.key from an object state variable and re-render.val from an array and re-render.HTML templates — fillWith()
/static/templates/{template}.html and render it once per
item in list. Returns a promise that resolves to the concatenated HTML string.
The template is a JS template literal — the item is exposed as element.
// static/templates/post.html
`<div class="post-card">
<h3>${element.title}</h3>
<p>${element.content}</p>
</div>`
import { fillWith } from '/static/framework/vesta.mjs';
const markup = await fillWith('post', posts);
document.getElementById('list').innerHTML = markup;
Output escaping (XSS)
Interpolation in templates (fillWith, getTemplate,
loadTemplate, and Subscribe content) is
raw by default — the engine stays flexible so you can compose
markup freely. That means you are responsible for escaping any
untrusted value before it lands in the DOM.
// Trusted / your own markup — interpolate freely:
`<ul>${fillWith('row', items)}</ul>`
// UNTRUSTED data (user input, query params, message bodies) — wrap in escapeHTML():
`<p>${escapeHTML(user.name)}</p>`
escapeHTML() (available globally) when interpolating it into a
template. Forgetting to escape untrusted input is an XSS vulnerability. Use
raw interpolation only for markup you control.
Navigation
All navigation helpers are in navigation.mjs.
target and inject it into element id.
selected tracks the active item: {category: "nav", id: "home"}.
postInsert is an optional callback run after injection.
async is true, loads the template first.target on first open.event is the click event.stepsID to show step step (0-indexed).
cible and reset the active state for id.import { goTo, toggleFM, gotoStep, hideLoadingScreen } from '/static/framework/navigation.mjs';
// Navigate to a section
document.getElementById('btn-posts').addEventListener('click', () => {
goTo('main-content', 'posts', { category: 'nav', id: 'posts' });
});
// Multi-step form
document.getElementById('btn-next').addEventListener('click', () => {
gotoStep(currentStep + 1, 'wizard');
});
// Init
hideLoadingScreen();
WebSocket client
/config. Authenticates with /authWS then hands off
incoming messages to /static/ws/onMessage.mjs.
// static/ws/onMessage.mjs — you write this
export function onMessage(data) {
const msg = JSON.parse(data);
switch (msg.type) {
case 'notification':
pushElement('notifications', msg.content);
break;
case 'update':
updateElement('feed', msg.payload);
break;
}
}
// main.mjs
import { initWebSockets } from '/static/framework/websockets.mjs';
initWebSockets(); // call once at startup
Internationalisation
Translation files live at static/translations/{lang}.mjs
and export a plain object of key → translated string.
// static/translations/en.mjs
export default {
welcome: "Welcome",
logout: "Sign out",
save: "Save changes",
};
// static/translations/fr.mjs
export default {
welcome: "Bienvenue",
logout: "Se déconnecter",
save: "Enregistrer",
};
import { loadTranslations, t } from '/static/translations/translation.mjs';
await loadTranslations('fr');
console.log(t('welcome')); // → "Bienvenue"
// In a template
`<button>${t('save')}</button>`
Utility functions
/logout and redirect to /auth./goodbye to delete the account.effect() when the user presses Enter. Use on input keydown handlers.options is passed to Intl.DateTimeFormat.
arrKeys but not in dict. Useful for diffing.import { checkEnter, getTimeStr, generateUUID } from '/static/framework/vesta.mjs';
// Submit on Enter
input.addEventListener('keydown', (e) => checkEnter(e, submitForm));
// Format a timestamp
const label = getTimeStr(post.created_at, { dateStyle: 'medium', timeStyle: 'short' });
// Generate an ID
const id = generateUUID(); // "550e8400-e29b-41d4-a716-446655440000"
Markdown renderer
A custom client-side parser. Syntax differs slightly from standard Markdown — designed for user-generated content with safe rendering.
import { render } from '/static/markdown/markdown.mjs';
const html = render("**bold** and _italic_ text\n\n> a blockquote");
document.getElementById('content').innerHTML = html;
| Syntax | Output |
|---|---|
**text** | Bold |
_text_ | Italic |
~~text~~ | Strikethrough |
`code` | Inline code |
```…``` | Code block |
> text | Blockquote |
||text|| | Spoiler (hidden until click) |
[label](url) | Link |