Odoo Web Library (OWL) is a modern, lightweight TypeScript UI framework for building reactive web applications with components, templates, and state management.
Virtual DOM implementation with block-based rendering, patching, and lifecycle management optimized for template-driven UIs.
The main BlockDOM API object containing core virtual DOM manipulation functions.
/**
* BlockDOM API object with core virtual DOM functions
*/
const blockDom: {
/** Configuration object */
config: Config;
/** Mount a VNode to DOM */
mount: (vnode: VNode, fixture: HTMLElement, afterNode?: Node) => void;
/** Patch one VNode with another */
patch: (vnode1: VNode, vnode2: VNode, withBeforeRemove?: boolean) => void;
/** Remove a VNode from DOM */
remove: (vnode: VNode, withBeforeRemove?: boolean) => void;
/** Create list block */
list: (items: any[], template: Function) => VNode;
/** Create multi-block container */
multi: (blocks: VNode[]) => VNode;
/** Create text node */
text: (value: string) => VNode;
/** Create comment node */
comment: (value: string) => VNode;
/** Create conditional block */
toggler: (condition: boolean, template: Function) => VNode;
/** Create generic block */
createBlock: (template: Function) => VNode;
/** Create raw HTML block */
html: (htmlString: string) => VNode;
};Mounts a virtual node to the DOM.
/**
* Mounts a VNode to a DOM element
* @param vnode - Virtual node to mount
* @param fixture - Target DOM element
* @param afterNode - Optional reference node for positioning
*/
function mount(vnode: VNode, fixture: HTMLElement, afterNode?: Node | null): void;Usage Examples:
import { blockDom } from "@odoo/owl";
// Mount a simple text node
const textNode = blockDom.text("Hello World");
blockDom.mount(textNode, document.body);
// Mount with positioning
const containerDiv = document.getElementById("container");
const existingElement = containerDiv.firstChild;
const newNode = blockDom.text("Insert before first child");
blockDom.mount(newNode, containerDiv, existingElement);
// Mount complex structures
const listNode = blockDom.list([1, 2, 3], (item) =>
blockDom.text(`Item: ${item}`)
);
blockDom.mount(listNode, document.getElementById("list-container"));Updates one virtual node with the content of another.
/**
* Patches vnode1 with the content of vnode2
* @param vnode1 - Target VNode to be updated
* @param vnode2 - Source VNode with new content
* @param withBeforeRemove - Whether to call beforeRemove hooks
*/
function patch(vnode1: VNode, vnode2: VNode, withBeforeRemove?: boolean): void;Usage Examples:
import { blockDom } from "@odoo/owl";
// Initial render
let currentNode = blockDom.text("Loading...");
blockDom.mount(currentNode, document.body);
// Update content
const newNode = blockDom.text("Content loaded!");
blockDom.patch(currentNode, newNode);
currentNode = newNode;
// Patch lists
let currentList = blockDom.list([1, 2, 3], (item) =>
blockDom.text(`Item ${item}`)
);
blockDom.mount(currentList, document.getElementById("list"));
// Update list with new items
const newList = blockDom.list([1, 2, 3, 4, 5], (item) =>
blockDom.text(`Updated Item ${item}`)
);
blockDom.patch(currentList, newList);
currentList = newList;
// Patch with cleanup
blockDom.patch(currentNode, newNode, true); // Calls beforeRemove hooksRemoves a virtual node from the DOM.
/**
* Removes a VNode from DOM
* @param vnode - Virtual node to remove
* @param withBeforeRemove - Whether to call beforeRemove hooks
*/
function remove(vnode: VNode, withBeforeRemove?: boolean): void;Usage Examples:
import { blockDom } from "@odoo/owl";
// Simple removal
const node = blockDom.text("Temporary content");
blockDom.mount(node, document.body);
setTimeout(() => {
blockDom.remove(node); // Remove after delay
}, 2000);
// Removal with cleanup
const complexNode = blockDom.multi([
blockDom.text("Part 1"),
blockDom.html("<div>Part 2</div>"),
blockDom.comment("End of content")
]);
blockDom.mount(complexNode, document.getElementById("temp-container"));
// Later remove with beforeRemove hooks
blockDom.remove(complexNode, true);Creates a text node block.
/**
* Creates a text node VNode
* @param value - Text content
* @returns Text VNode
*/
function text(value: string): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Simple text
const greeting = blockDom.text("Hello, World!");
blockDom.mount(greeting, document.body);
// Dynamic text content
const counter = { value: 0 };
const updateCounter = () => {
const newText = blockDom.text(`Count: ${counter.value}`);
blockDom.patch(currentText, newText);
currentText = newText;
};
let currentText = blockDom.text(`Count: ${counter.value}`);
blockDom.mount(currentText, document.getElementById("counter"));
// Update every second
setInterval(() => {
counter.value++;
updateCounter();
}, 1000);Creates a comment node block.
/**
* Creates a comment node VNode
* @param value - Comment text
* @returns Comment VNode
*/
function comment(value: string): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Debug comments
const debugComment = blockDom.comment("Debug: Component rendered at " + new Date());
blockDom.mount(debugComment, document.body);
// Conditional comments
const renderWithComments = (showComments, data) => {
const blocks = [];
if (showComments) {
blocks.push(blockDom.comment("Data rendering start"));
}
blocks.push(blockDom.text(JSON.stringify(data)));
if (showComments) {
blocks.push(blockDom.comment("Data rendering end"));
}
return blockDom.multi(blocks);
};Creates a block with raw HTML content.
/**
* Creates a raw HTML VNode
* @param htmlString - Raw HTML string
* @returns HTML VNode
*/
function html(htmlString: string): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Simple HTML
const richContent = blockDom.html("<p><strong>Bold</strong> and <em>italic</em> text</p>");
blockDom.mount(richContent, document.getElementById("content"));
// HTML from markdown or other sources
const markdownHTML = "<h1>Title</h1><p>Some content with <a href='#'>links</a></p>";
const markdownBlock = blockDom.html(markdownHTML);
blockDom.mount(markdownBlock, document.getElementById("markdown-content"));
// Dynamic HTML content
const renderHTML = (data) => {
const html = `
<div class="card">
<h2>${data.title}</h2>
<div class="content">${data.htmlContent}</div>
<footer>Created: ${data.createdAt}</footer>
</div>
`;
return blockDom.html(html);
};
const htmlBlock = renderHTML({
title: "Article Title",
htmlContent: "<p>This is <strong>rich content</strong>.</p>",
createdAt: "2024-01-01"
});Creates a list block for rendering arrays of data.
/**
* Creates a list VNode for rendering arrays
* @param items - Array of items to render
* @param template - Function that creates VNode for each item
* @returns List VNode
*/
function list<T>(items: T[], template: (item: T, index: number) => VNode): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Simple list
const numbers = [1, 2, 3, 4, 5];
const numberList = blockDom.list(numbers, (num, index) =>
blockDom.text(`${index + 1}. ${num}`)
);
blockDom.mount(numberList, document.getElementById("numbers"));
// Complex object list
const users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
{ id: 3, name: "Charlie", email: "charlie@example.com" }
];
const userList = blockDom.list(users, (user) => {
const userHTML = `
<div class="user-card" data-user-id="${user.id}">
<h3>${user.name}</h3>
<p>${user.email}</p>
</div>
`;
return blockDom.html(userHTML);
});
blockDom.mount(userList, document.getElementById("user-list"));
// Nested lists
const categories = [
{
name: "Fruits",
items: ["Apple", "Banana", "Orange"]
},
{
name: "Vegetables",
items: ["Carrot", "Broccoli", "Spinach"]
}
];
const categoryList = blockDom.list(categories, (category) => {
const categoryHeader = blockDom.html(`<h2>${category.name}</h2>`);
const itemList = blockDom.list(category.items, (item) =>
blockDom.html(`<li>${item}</li>`)
);
return blockDom.multi([
categoryHeader,
blockDom.html("<ul>"),
itemList,
blockDom.html("</ul>")
]);
});Creates a container for multiple blocks.
/**
* Creates a multi-block container
* @param blocks - Array of VNodes to group together
* @returns Multi VNode containing all blocks
*/
function multi(blocks: VNode[]): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Group related elements
const header = blockDom.html("<h1>Welcome</h1>");
const content = blockDom.text("This is the main content.");
const footer = blockDom.html("<footer>© 2024</footer>");
const page = blockDom.multi([header, content, footer]);
blockDom.mount(page, document.body);
// Conditional multi-blocks
const renderPage = (user, showHeader = true, showFooter = true) => {
const blocks = [];
if (showHeader) {
blocks.push(blockDom.html(`<header><h1>Welcome, ${user.name}!</h1></header>`));
}
blocks.push(blockDom.html(`
<main>
<p>User ID: ${user.id}</p>
<p>Email: ${user.email}</p>
</main>
`));
if (showFooter) {
blocks.push(blockDom.html("<footer>Thank you for visiting!</footer>"));
}
return blockDom.multi(blocks);
};
// Dynamic multi-blocks
const renderDashboard = (widgets) => {
const blocks = [
blockDom.html("<div class='dashboard-header'><h1>Dashboard</h1></div>")
];
widgets.forEach(widget => {
blocks.push(blockDom.html(`
<div class="widget" data-widget-id="${widget.id}">
<h2>${widget.title}</h2>
<div class="widget-content">${widget.content}</div>
</div>
`));
});
blocks.push(blockDom.html("<div class='dashboard-footer'>Last updated: " + new Date().toLocaleString() + "</div>"));
return blockDom.multi(blocks);
};Creates a conditional block that shows/hides content.
/**
* Creates a conditional block
* @param condition - Boolean condition for showing content
* @param template - Function that creates VNode when condition is true
* @returns Toggler VNode
*/
function toggler(condition: boolean, template: () => VNode): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Simple conditional rendering
let showMessage = false;
const messageToggler = blockDom.toggler(showMessage, () =>
blockDom.text("This message is conditionally shown!")
);
blockDom.mount(messageToggler, document.getElementById("message-area"));
// Toggle visibility
document.getElementById("toggle-button").onclick = () => {
showMessage = !showMessage;
const newToggler = blockDom.toggler(showMessage, () =>
blockDom.text("This message is conditionally shown!")
);
blockDom.patch(messageToggler, newToggler);
};
// Complex conditional content
const user = { isAdmin: false, name: "John" };
const adminPanel = blockDom.toggler(user.isAdmin, () => {
const adminContent = `
<div class="admin-panel">
<h2>Admin Panel</h2>
<button>Manage Users</button>
<button>System Settings</button>
<button>View Logs</button>
</div>
`;
return blockDom.html(adminContent);
});
// Loading states
let isLoading = true;
const loadingToggler = blockDom.toggler(isLoading, () =>
blockDom.html("<div class='spinner'>Loading...</div>")
);
const content = blockDom.multi([
loadingToggler,
blockDom.toggler(!isLoading, () =>
blockDom.html("<div>Content loaded successfully!</div>")
)
]);
// Simulate loading completion
setTimeout(() => {
isLoading = false;
// Re-render with updated condition
}, 2000);Creates a generic block from a template function.
/**
* Creates a generic block from template function
* @param template - Function that generates DOM structure
* @returns Generic block VNode
*/
function createBlock(template: () => HTMLElement | string): VNode;Usage Examples:
import { blockDom } from "@odoo/owl";
// Create custom block
const customBlock = blockDom.createBlock(() => {
const div = document.createElement("div");
div.className = "custom-widget";
div.innerHTML = "<p>Custom widget content</p>";
// Add event listeners
div.addEventListener("click", () => {
console.log("Custom widget clicked!");
});
return div;
});
blockDom.mount(customBlock, document.getElementById("widget-container"));
// Block with dynamic content
const dynamicBlock = (data) => blockDom.createBlock(() => {
const container = document.createElement("div");
container.className = "dynamic-content";
// Create elements programmatically
const title = document.createElement("h2");
title.textContent = data.title;
container.appendChild(title);
const list = document.createElement("ul");
data.items.forEach(item => {
const listItem = document.createElement("li");
listItem.textContent = item;
list.appendChild(listItem);
});
container.appendChild(list);
return container;
});
// Chart integration
const chartBlock = (chartData) => blockDom.createBlock(() => {
const canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 300;
// Initialize chart library
const ctx = canvas.getContext("2d");
new ThirdPartyChart(ctx, chartData);
return canvas;
});The core virtual node interface that all BlockDOM elements implement.
/**
* Virtual node interface implemented by all BlockDOM elements
*/
interface VNode<T = any> {
/** Mounts the VNode to a parent element */
mount(parent: HTMLElement, afterNode: Node | null): void;
/** Moves the VNode before a DOM node */
moveBeforeDOMNode(node: Node | null, parent?: HTMLElement): void;
/** Moves the VNode before another VNode */
moveBeforeVNode(other: T | null, afterNode: Node | null): void;
/** Patches this VNode with another VNode */
patch(other: T, withBeforeRemove: boolean): void;
/** Called before removal for cleanup */
beforeRemove(): void;
/** Removes the VNode from DOM */
remove(): void;
/** Gets the first DOM node of this VNode */
firstNode(): Node | undefined;
/** Associated DOM element (if any) */
el?: HTMLElement | Text;
/** Parent DOM element */
parentEl?: HTMLElement;
/** Whether this is the only child */
isOnlyChild?: boolean;
/** Unique key for list optimizations */
key?: any;
}
/**
* Type alias for any BlockDOM VNode
*/
type BDom = VNode<any>;Install with Tessl CLI
npx tessl i tessl/npm-odoo--owl