vidoriel
v0.1.4
Published
Virtual dom template engine to create html and javascript user interfaces and render them server- and browser-side.
Downloads
105
Readme
Vidoriel
Vidoriel is a virtual DOM template system with strong focus on server-side rendering (SSR). Its unique strength is seamlessly handling the same .vdrl templates across server-side rendering, client-side hydration, and browser reactivity with the same codebase.
Installation
npm install vidorielQuick Start - Full SSR Workflow
1. Create templates
index.vdrl
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
{ssr}
</head>
<body>
{content}
<script type="module" src="/client.js"></script>
</body>
</html>greeting.vdrl
<app-container>
<div class="greeting">
<h1>Hello {name}!</h1>
{if items}
<ul>
{each items}
<li>{$i} of {$l}: {$v}</li>
{/each}
</ul>
{/if}
<form :submit>
<button type="submit">Add Item</button>
</form>
</div>
</app-container>2. Server-side rendering (generates HTML + serves modules):
server.js
// Node.js: run with --import flag
// Bun: bunfig.toml handles .vdrl imports
import GreetingTemplate from './greeting.vdrl';
import IndexTemplate from './index.vdrl';
import { createServer } from 'http';
import { readFile } from 'fs/promises';
import { compile } from 'vidoriel';
const server = createServer(async (req, res) => {
// Serve compiled .vdrl files as JavaScript modules to the browser
if (req.url.endsWith('.vdrl')) {
const vdrlContent = await readFile(`.${req.url}`, 'utf8');
const jsModule = await compile(vdrlContent);
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.end(jsModule);
return;
}
// Prepare data
const data = {
name: 'World',
items: ['Apple', 'Banana', 'Cherry'],
};
// Render content component
const content = new GreetingTemplate(data);
// Render full page with Vidoriel (including data for client hydration)
const index = new IndexTemplate({
content,
ssr: '<script>globalThis.ssr = ' + JSON.stringify(data) + ';</script>'
});
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(index.html());
});3. Client-side hydration (makes it interactive):
client.js
import GreetingTemplate from './greeting.vdrl';
const app = new GreetingTemplate({
...globalThis.ssr, // Reuse server-rendered data
// Add interactivity
submit(formData) {
// formData contains form inputs (if any)
const fruits = ['Orange', 'Mango', 'Kiwi', 'Pineapple'];
const randomFruit = fruits[Math.floor(Math.random() * fruits.length)];
this.items.push(randomFruit); // Automatically updates DOM!
}
});
// Hydrate existing server-rendered HTML
app.mount(document.querySelector('app-container'));The magic: Same template, same data, works perfectly on server AND client!
📁 See complete working example: Runnable server + client demo Bun: examples/ssr-workflow-bun/ NodeJS: examples/ssr-workflow-node/
💡 Learn more about SSR patterns: Component Patterns Guide
Variables
Basic Variables
{variable} <!-- Simple variable -->
{variable.sub.prop} <!-- Nested object access -->
{user.name} <!-- Object properties -->Variable Types
{variable} <!-- Optional (empty string if undefined) -->
{!requiredVar} <!-- Required (throws error if undefined) -->Undefined variables resolve to empty strings. Use {!var} to throw an error if a variable is supposed to exist.
Control structures
if / elif / else
Full JavaScript conditions
{if user.id === 65 && group === 98}
<p>Admin user in special group</p>
{elif user.active}
<p>Active user</p>
{else}
<p>Inactive user</p>
{/if}Express condition syntax
{if arr} <!-- Checks arr.length > 0 for arrays -->
{if variable} <!-- Checks typeof variable !== 'undefined' && !!variable -->
{if !variable} <!-- Negated variable check -->each
Simple array
const fruits = ['Apple', 'Banana', 'Orange', 'Cherry'];<fruit-list>
{each fruits}
<fruit-item>
<fruit-index>{$i}</fruit-index> <!-- Index: 0, 1, 2, 3 -->
<fruit-name>{$v}</fruit-name> <!-- Value: Apple, Banana, etc. -->
<fruit-total>{$l}</fruit-total> <!-- Length: 4 -->
</fruit-item>
{/each}
</fruit-list>Array of objects
const users = [
{ name: 'Alice', slug: 'alice', active: true },
{ name: 'Bob', slug: 'bob', active: false }
];<user-list>
{each users}
<user-item class={active ? 'active' : false}> <!-- false means the attribute will not be shown -->
<user-index>{$i}</user-index> <!-- $i works here too -->
<a href="users/{slug}"><user-name>{name}</user-name></a> <!-- The properties of an item are directly accessible -->
<user-name>{$v.name}</user-name> <!-- $v is the value, so you can call name like this too -->
</user-item>
{/each}
</user-list>Events
Vidoriel supports event binding with the : prefix:
<button :click=handleClick>Click me</button>
<form :submit=handleSubmit>
<input :bind=username placeholder="Enter name">
<input type="submit" value="Submit">
</form><button :click> <!-- shorthand for :click=click -->Event Types
:click- Click events:submit- Form submission:input- Input changes:bind- Two-way data binding for form inputs- Any standard DOM event (
:mouseover,:keydown, etc.)
Event Handler Context
Event handlers receive a context object:
{ event: DOMEvent, // The original DOM event
element: DOMElement, // The target element
vNode: VirtualNode // The virtual DOM node
}import Template from './greeting.vdrl';
const greeting = new Template({
name: 'World',
click({ event, element, vNode }) {
alert('Clicked!');
},
});An exception is the :submit event, which provides the form data as an object as first argument
submit(formData, { event, element, vNode }) {
console.log(formData); // { username: 'John' }
}💡 Learn more: See Event Handling Examples for advanced patterns
Components
How to Organize (Choose Your Style)
1. Factory Functions (quick & simple)
import Vidoriel from 'vidoriel';
export function Card(data) {
return new Vidoriel(`<div>{title}</div>`, {
title: 'Hello',
click() {
alert('Clicked!');
},
});
}2. Import .vdrl Files (clean separation)
see detailed explanation below in build integration
import Template from './card.vdrl';
const card = new Template({
title: 'Hello',
click() {
alert('Clicked!');
},
});3. Convention-Based (scalable apps)
- /components/card/card.vdrl <- view
- /components/card/card.controller.js <- controller
- /components/card/card.provider.js <- provider of data
Your framework auto-wires them for example like this (but probably automated by looping through the directories):
import CardView from './components/views/card.vdrl';
import CardController from './components/controllers/card.controller.js';
import CardProvider from './components/providers/card.provider.js';
const provider = new CardProvider(); // data provider
const controller = new CardController(provider.fetchSomeData()); // controller
const card = new CardView(controller); // view
card.mount(document.querySelector('app-container'));📖 See Component Patterns Guide for detailed patterns and architecture examples.
How Components Work Together
Sub-Components (independent instances with own state):
const button = new Vidoriel(`<button :click=handleClick>Click</button>`, {
handleClick() { console.log('Clicked!'); }
});
const page = new Vidoriel(`<div>{button}</div>`, { button });
// button keeps its own state and handlersSub-Templates (shared markup, inherits parent data):
globalThis.componentStore = {
tags: { 'user-card': '/path/to/card-template' },
paths: { '/path/to/card-template': parse(`<div>{name}</div>`) }
}; // our nodejs and bun plugin does this automatically
const app = new Vidoriel(`
{each users}
<user-card />
{/each}
`, { users: [...] }, componentStore);
// each card inherits data from parent's loop though the users with each💡 Deep dive: Component Patterns Guide covers all organization patterns, sub-components vs sub-templates, nested components, and best practices
Localization
Support for internationalization with custom translate function:
// Set up custom translate function by overwriting Vidoriel.translate
Vidoriel.translate = (key, data, ...args) => {
const translations = {
welcome_message: 'Welcome to our app!',
greeting: 'Hello {0}, you have {1} messages'
};
let text = translations[key] || key;
// Replace {0}, {1}, etc. with args
args.forEach((arg, i) => {
text = text.replace(`{${i}}`, arg);
});
return text;
};{#welcome_message} <!-- Uses translate function -->
{#greeting name messageCount} <!-- Passes variables name and messageCount as args -->API Reference
Vidoriel Class
const app = new Vidoriel(template, data, componentTree);Methods
html()- Generate HTML string (server-side)mount(element)- Hydrate/mount to DOM (client-side)patch()- Manually trigger re-renderrender()- Get virtual DOM tree
Properties
data- Reactive data object (auto-triggers patches)virtualDOM- Current virtual DOM state
Data Reactivity
Data is automatically wrapped in proxies for reactivity:
app.data.username = 'Alice'; // Automatically triggers DOM update
app.data.items.push('New Item'); // Array changes also trigger updatesHow It Works
Server-Side Rendering
- Build plugins compile
.vdrl→ JavaScript modules - Use
.html()method to generate HTML strings - Perfect for HTTP responses, static site generation
// Server generates HTML
const html = app.html(); // Returns: '<div class="greeting">...</div>'Client-Side Hydration
- Same
.vdrlimports work in browser - Use
.mount()to attach to existing DOM - Automatic reactivity and virtual DOM diffing
// Browser makes it interactive
app.mount(document.querySelector('#app'));
app.data.name = 'Alice'; // DOM updates automatically!Zero Configuration SSR
- No build step needed - plugins handle everything
- Same templates work server AND client
- Seamless hydration - no mismatches or flashes
- Full reactivity after hydration
Direct Template Usage
You can also use template strings without .vdrl files:
import Vidoriel from 'vidoriel';
const app = new Vidoriel(`
<div>Hello {name}!</div>
`, { name: 'World' });
// Server: app.html()
// Browser: app.mount(element)Build Integration
Vidoriel's build plugins are essential for its preferred SSR workflow - they make .vdrl files work as JavaScript modules in both server and browser environments.
What the plugins do:
- Server: Compile
.vdrl→ JS modules forimportstatements - Browser: Server responds with
text/javascriptheader + compiled JS - Seamless: Same
import MyComponent from './component.vdrl'works everywhere - Zero config: No webpack, no build step, just works
Bun
# bunfig.toml
preload = ["vidoriel/bun-preload.js"]Node.js (v20+)
node --import=vidoriel/nodejs-plugin-import.js your-script.jsNode.js (Legacy)
node --loader=vidoriel/nodejs-plugin.js your-script.jsError Codes
Renderer Errors
- 01: Template error evaluating condition
- 02: Mandatory variable not found
- 03: Tried to use function as var
- 04: Weird render tree, maybe accessing object as var?
- 05: Invalid variable node
DOM Errors
- 06: Unknown node
- 07: insertBefore error
- 08: removeChild error
Parser Errors
- 09: Closing tag mismatch
- 10: DOM Stack empty
Patcher Errors
- 11: No real dom parentNode
Component Errors
- 12: Can't patch component that never was mounted (auto-prevents default)
License
MIT License: You are free to use, modify, and distribute this software, but it comes with no warranty. Do not use the exact same name or imply that your fork or application is directly affiliated with Vidoriel or Hybrilios.
