CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-odoo--owl

Odoo Web Library (OWL) is a modern, lightweight TypeScript UI framework for building reactive web applications with components, templates, and state management.

Overview
Eval results
Files

utils-validation.mddocs/

Utilities & Validation

Utility functions for event handling, DOM manipulation, data loading, and component validation.

Capabilities

Utility Functions

batched

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();
  }
}

EventBus

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);
    });
  }
}

htmlEscape

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); // &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

// 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;
    }
  }
}

whenReady

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();
  }
});

loadFile

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;
      }, {});
    });
  }
}

markup

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>
  `;
};

Validation System

validate

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}`);
    }
  }
}

validateType

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);
  }
}

Schema Types

/**
 * 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;
}

Validation Examples

// 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

docs

app-components.md

blockdom.md

hooks.md

index.md

lifecycle.md

reactivity.md

templates.md

utils-validation.md

tile.json