Odoo Web Library (OWL) is a modern, lightweight TypeScript UI framework for building reactive web applications with components, templates, and state management.
Utility functions for event handling, DOM manipulation, data loading, and component validation.
Creates a batched version of a callback to prevent multiple executions in the same microtick.
/**
* Creates a batched version of a callback
* @param callback - Function to batch
* @returns Batched version that executes once per microtick
*/
function batched(callback: () => void): () => void;Usage Examples:
import { batched } from "@odoo/owl";
// Prevent multiple rapid updates
const updateUI = batched(() => {
console.log("UI updated!");
document.getElementById("status").textContent = "Updated at " + new Date();
});
// Multiple calls in same microtick will only execute once
updateUI();
updateUI();
updateUI(); // Only one "UI updated!" will be logged
// Batch expensive operations
const expensiveOperation = batched(() => {
// Heavy computation or DOM manipulation
recalculateLayout();
updateCharts();
refreshDataViews();
});
// In event handlers
document.addEventListener("resize", batched(() => {
handleResize();
}));
// Batch state updates
class Component extends Component {
setup() {
this.batchedRender = batched(() => {
this.render();
});
}
updateMultipleValues() {
this.state.value1 = "new1";
this.state.value2 = "new2";
this.state.value3 = "new3";
// Only trigger one render
this.batchedRender();
}
}Event system for component communication and global event handling.
/**
* Event bus for publish-subscribe pattern
*/
class EventBus extends EventTarget {
/**
* Trigger a custom event
* @param name - Name of the event to trigger
* @param payload - Data payload for the event (optional)
*/
trigger(name: string, payload?: any): void;
}Usage Examples:
import { EventBus } from "@odoo/owl";
// Global event bus
const globalEventBus = new EventBus();
// Component communication
class NotificationService {
constructor() {
this.eventBus = new EventBus();
}
show(message, type = "info") {
this.eventBus.trigger("notification:show", { message, type, id: Date.now() });
}
hide(id) {
this.eventBus.trigger("notification:hide", { id });
}
}
class NotificationComponent extends Component {
setup() {
this.notifications = useState([]);
// Subscribe to notification events
notificationService.eventBus.addEventListener("notification:show", (event) => {
this.notifications.push(event.detail);
// Auto-hide after delay
setTimeout(() => {
this.hideNotification(event.detail.id);
}, 5000);
});
notificationService.eventBus.addEventListener("notification:hide", (event) => {
this.hideNotification(event.detail.id);
});
}
hideNotification(id) {
const index = this.notifications.findIndex(n => n.id === id);
if (index >= 0) {
this.notifications.splice(index, 1);
}
}
}
// Global state management
class UserService {
constructor() {
this.eventBus = new EventBus();
this.currentUser = null;
}
login(user) {
this.currentUser = user;
this.eventBus.trigger("user:login", user);
}
logout() {
const user = this.currentUser;
this.currentUser = null;
this.eventBus.trigger("user:logout", user);
}
}
// Multiple components can listen to user events
class HeaderComponent extends Component {
setup() {
userService.eventBus.addEventListener("user:login", (event) => {
this.render(); // Re-render header with user info
});
userService.eventBus.addEventListener("user:logout", (event) => {
this.render(); // Re-render header without user info
});
}
}
// Cleanup subscriptions
class TemporaryComponent extends Component {
setup() {
this.handleUserLogin = (event) => {
console.log("User logged in:", event.detail);
};
globalEventBus.addEventListener("user:login", this.handleUserLogin);
onWillDestroy(() => {
// Clean up subscriptions
globalEventBus.removeEventListener("user:login", this.handleUserLogin);
});
}
}Escapes HTML special characters to prevent XSS attacks.
/**
* Escapes HTML special characters
* @param str - String to escape
* @returns HTML-escaped string
*/
function htmlEscape(str: string): string;Usage Examples:
import { htmlEscape } from "@odoo/owl";
// Escape user input
const userInput = '<script>alert("XSS")</script>';
const safeHTML = htmlEscape(userInput);
console.log(safeHTML); // <script>alert("XSS")</script>
// Safe display of user content
class CommentComponent extends Component {
static template = xml`
<div class="comment">
<h4><t t-esc="props.comment.author" /></h4>
<div t-raw="safeContent" />
<small><t t-esc="formattedDate" /></small>
</div>
`;
get safeContent() {
// Escape HTML but allow some basic formatting
let content = htmlEscape(this.props.comment.content);
// Optionally allow some safe HTML after escaping
content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
content = content.replace(/\*(.*?)\*/g, '<em>$1</em>');
return content;
}
get formattedDate() {
return new Date(this.props.comment.createdAt).toLocaleDateString();
}
}
// Form validation with safe error display
class FormComponent extends Component {
validateAndShowError(input, errorContainer) {
const value = input.value;
let error = null;
if (value.length < 3) {
error = "Must be at least 3 characters";
} else if (value.includes("<script>")) {
error = "Invalid characters detected";
}
if (error) {
// Safely display error (though this is a controlled string)
errorContainer.textContent = htmlEscape(error);
errorContainer.style.display = "block";
return false;
} else {
errorContainer.style.display = "none";
return true;
}
}
}Executes a callback when the DOM is ready.
/**
* Executes callback when DOM is ready
* @param callback - Function to execute when DOM is ready
*/
function whenReady(callback: () => void): void;Usage Examples:
import { whenReady, mount, Component, xml } from "@odoo/owl";
// Wait for DOM before mounting app
class App extends Component {
static template = xml`<div>App is ready!</div>`;
}
whenReady(() => {
mount(App, document.body);
});
// Initialize scripts after DOM is ready
whenReady(() => {
// Initialize third-party libraries
initAnalytics();
setupGlobalErrorHandling();
loadUserPreferences();
// Setup global event listeners
document.addEventListener("keydown", handleGlobalKeyDown);
window.addEventListener("beforeunload", handleBeforeUnload);
});
// Conditional initialization
whenReady(() => {
if (document.getElementById("owl-app")) {
// Mount OWL app
mount(MainApp, document.getElementById("owl-app"));
}
if (document.getElementById("legacy-widget")) {
// Initialize legacy jQuery widget
$("#legacy-widget").widget();
}
});Loads a file from a URL asynchronously.
/**
* Loads a file from URL
* @param url - URL to load
* @returns Promise resolving to file content as string
*/
function loadFile(url: string): Promise<string>;Usage Examples:
import { loadFile, Component, xml } from "@odoo/owl";
// Load template files
class TemplateLoader extends Component {
setup() {
this.state = useState({
template: null,
loading: true,
error: null
});
onWillStart(async () => {
try {
const templateContent = await loadFile("/templates/custom-template.xml");
this.state.template = templateContent;
} catch (error) {
this.state.error = "Failed to load template";
} finally {
this.state.loading = false;
}
});
}
}
// Load configuration files
class ConfigurableComponent extends Component {
setup() {
this.config = null;
onWillStart(async () => {
try {
const configJSON = await loadFile("/config/app-config.json");
this.config = JSON.parse(configJSON);
} catch (error) {
// Use default config
this.config = { theme: "light", lang: "en" };
}
});
}
}
// Load CSS dynamically
async function loadTheme(themeName) {
try {
const cssContent = await loadFile(`/themes/${themeName}.css`);
// Inject CSS into page
const style = document.createElement("style");
style.textContent = cssContent;
document.head.appendChild(style);
return true;
} catch (error) {
console.error("Failed to load theme:", error);
return false;
}
}
// Load and parse data files
class DataViewer extends Component {
setup() {
this.state = useState({ data: null, loading: true });
onWillStart(async () => {
try {
const csvContent = await loadFile("/data/sample-data.csv");
this.state.data = this.parseCSV(csvContent);
} catch (error) {
this.state.data = [];
} finally {
this.state.loading = false;
}
});
}
parseCSV(csvText) {
const lines = csvText.split('\n');
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, index) => {
obj[header.trim()] = values[index]?.trim() || '';
return obj;
}, {});
});
}
}Template literal tag for creating markup strings.
/**
* Template literal tag for markup strings
* @param template - Template string parts
* @param args - Interpolated values
* @returns Markup string
*/
function markup(template: TemplateStringsArray, ...args: any[]): string;Usage Examples:
import { markup, htmlEscape } from "@odoo/owl";
// Safe markup creation
const createUserCard = (user) => {
const safeUserName = htmlEscape(user.name);
const safeEmail = htmlEscape(user.email);
return markup`
<div class="user-card" data-user-id="${user.id}">
<h3>${safeUserName}</h3>
<p>${safeEmail}</p>
<button onclick="editUser(${user.id})">Edit</button>
</div>
`;
};
// Dynamic content generation
const createTable = (data, columns) => {
const headerRow = markup`
<tr>
${columns.map(col => `<th>${htmlEscape(col.title)}</th>`).join('')}
</tr>
`;
const dataRows = data.map(row => {
const cells = columns.map(col => {
const value = row[col.field] || '';
return `<td>${htmlEscape(String(value))}</td>`;
}).join('');
return markup`<tr>${cells}</tr>`;
}).join('');
return markup`
<table class="data-table">
<thead>${headerRow}</thead>
<tbody>${dataRows}</tbody>
</table>
`;
};
// Email template generation
const createEmailTemplate = (user, data) => {
return markup`
<!DOCTYPE html>
<html>
<head>
<title>Welcome ${htmlEscape(user.name)}</title>
</head>
<body>
<h1>Welcome, ${htmlEscape(user.name)}!</h1>
<p>Thank you for joining our platform.</p>
<ul>
${data.features.map(feature =>
`<li>${htmlEscape(feature)}</li>`
).join('')}
</ul>
<p>Best regards,<br>The Team</p>
</body>
</html>
`;
};Validates a value against a schema with detailed error reporting.
/**
* Validates an object against a schema
* @param obj - Object to validate
* @param spec - Schema specification
* @throws OwlError if validation fails
*/
function validate(obj: { [key: string]: any }, spec: Schema): void;
/**
* Helper validation function that returns list of errors
* @param obj - Object to validate
* @param schema - Schema specification
* @returns Array of error messages
*/
function validateSchema(obj: { [key: string]: any }, schema: Schema): string[];Usage Examples:
import { validate, Component } from "@odoo/owl";
// Component props validation
class UserProfile extends Component {
static props = {
user: {
type: Object,
shape: {
id: Number,
name: String,
email: String,
age: { type: Number, optional: true }
}
},
showEmail: { type: Boolean, optional: true }
};
setup() {
// Manual validation in development
if (this.env.dev) {
try {
validate("user", this.props.user, UserProfile.props.user);
console.log("Props validation passed");
} catch (error) {
console.error("Props validation failed:", error.message);
}
}
}
}
// Form validation
class ContactForm extends Component {
validateForm() {
const formData = {
name: this.refs.nameInput.value,
email: this.refs.emailInput.value,
age: parseInt(this.refs.ageInput.value) || null,
newsletter: this.refs.newsletterCheckbox.checked
};
const schema = {
name: String,
email: String,
age: { type: Number, optional: true },
newsletter: Boolean
};
try {
validate("contactForm", formData, schema);
this.submitForm(formData);
return true;
} catch (error) {
this.showError(`Validation failed: ${error.message}`);
return false;
}
}
}
// API response validation
class DataService {
async fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
// Validate API response structure
const userSchema = {
id: Number,
name: String,
email: String,
profile: {
type: Object,
optional: true,
shape: {
avatar: { type: String, optional: true },
bio: { type: String, optional: true }
}
},
permissions: {
type: Array,
element: String
}
};
try {
validate("apiUser", userData, userSchema);
return userData;
} catch (error) {
throw new Error(`Invalid user data from API: ${error.message}`);
}
}
}Validates a value against a type description.
/**
* Validates a value against a type description
* @param key - Key name for error messages
* @param value - Value to validate
* @param descr - Type description to validate against
* @returns Error message string or null if valid
*/
function validateType(key: string, value: any, descr: TypeDescription): string | null;Usage Examples:
import { validateType } from "@odoo/owl";
// Basic type checking
console.log(validateType("hello", String)); // true
console.log(validateType(42, Number)); // true
console.log(validateType(true, Boolean)); // true
console.log(validateType([], Array)); // true
console.log(validateType({}, Object)); // true
// Complex type validation
const userType = {
type: Object,
shape: {
id: Number,
name: String,
active: Boolean
}
};
const validUser = { id: 1, name: "John", active: true };
const invalidUser = { id: "1", name: "John", active: "yes" };
console.log(validateType(validUser, userType)); // true
console.log(validateType(invalidUser, userType)); // false
// Array validation
const numberArrayType = {
type: Array,
element: Number
};
console.log(validateType([1, 2, 3], numberArrayType)); // true
console.log(validateType([1, "2", 3], numberArrayType)); // false
// Optional fields
const optionalFieldType = {
type: Object,
shape: {
required: String,
optional: { type: Number, optional: true }
}
};
console.log(validateType({ required: "yes" }, optionalFieldType)); // true
console.log(validateType({ required: "yes", optional: 42 }, optionalFieldType)); // true
console.log(validateType({ optional: 42 }, optionalFieldType)); // false (missing required)
// Union types
const unionType = [String, Number];
console.log(validateType("hello", unionType)); // true
console.log(validateType(42, unionType)); // true
console.log(validateType(true, unionType)); // false
// Dynamic type checking
class FormField extends Component {
validateInput(value) {
let type;
switch (this.props.fieldType) {
case "text":
type = String;
break;
case "number":
type = Number;
break;
case "email":
type = { type: String, validate: (v) => v.includes("@") };
break;
default:
type = true; // Accept any type
}
return validateType(value, type);
}
}/**
* Validation schema types
*/
type Schema = string[] | { [key: string]: TypeDescription };
type TypeDescription = BaseType | TypeInfo | ValueType | TypeDescription[];
type BaseType = { new (...args: any[]): any } | true | "*";
interface TypeInfo {
/** Type to validate against */
type?: TypeDescription;
/** Whether the field is optional */
optional?: boolean;
/** Custom validation function */
validate?: (value: any) => boolean;
/** Object shape for Object types */
shape?: Schema;
/** Element type for Array types */
element?: TypeDescription;
/** Value type for Map-like objects */
values?: TypeDescription;
}
interface ValueType {
/** Exact value to match */
value: any;
}// Component with comprehensive validation
class ProductForm extends Component {
static props = {
product: {
type: Object,
shape: {
id: { type: Number, optional: true },
name: String,
price: Number,
category: ["electronics", "books", "clothing"], // Union of specific values
tags: { type: Array, element: String },
metadata: {
type: Object,
optional: true,
shape: {
weight: { type: Number, optional: true },
dimensions: {
type: Object,
optional: true,
shape: {
width: Number,
height: Number,
depth: Number
}
}
}
}
}
},
onSave: Function,
readonly: { type: Boolean, optional: true }
};
}Install with Tessl CLI
npx tessl i tessl/npm-odoo--owl