or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

app-components.mdblockdom.mdhooks.mdindex.mdlifecycle.mdreactivity.mdtemplates.mdutils-validation.md

utils-validation.mddocs/

0

# Utilities & Validation

1

2

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

3

4

## Capabilities

5

6

### Utility Functions

7

8

#### batched

9

10

Creates a batched version of a callback to prevent multiple executions in the same microtick.

11

12

```typescript { .api }

13

/**

14

* Creates a batched version of a callback

15

* @param callback - Function to batch

16

* @returns Batched version that executes once per microtick

17

*/

18

function batched(callback: () => void): () => void;

19

```

20

21

**Usage Examples:**

22

23

```typescript

24

import { batched } from "@odoo/owl";

25

26

// Prevent multiple rapid updates

27

const updateUI = batched(() => {

28

console.log("UI updated!");

29

document.getElementById("status").textContent = "Updated at " + new Date();

30

});

31

32

// Multiple calls in same microtick will only execute once

33

updateUI();

34

updateUI();

35

updateUI(); // Only one "UI updated!" will be logged

36

37

// Batch expensive operations

38

const expensiveOperation = batched(() => {

39

// Heavy computation or DOM manipulation

40

recalculateLayout();

41

updateCharts();

42

refreshDataViews();

43

});

44

45

// In event handlers

46

document.addEventListener("resize", batched(() => {

47

handleResize();

48

}));

49

50

// Batch state updates

51

class Component extends Component {

52

setup() {

53

this.batchedRender = batched(() => {

54

this.render();

55

});

56

}

57

58

updateMultipleValues() {

59

this.state.value1 = "new1";

60

this.state.value2 = "new2";

61

this.state.value3 = "new3";

62

// Only trigger one render

63

this.batchedRender();

64

}

65

}

66

```

67

68

#### EventBus

69

70

Event system for component communication and global event handling.

71

72

```typescript { .api }

73

/**

74

* Event bus for publish-subscribe pattern

75

*/

76

class EventBus extends EventTarget {

77

/**

78

* Trigger a custom event

79

* @param name - Name of the event to trigger

80

* @param payload - Data payload for the event (optional)

81

*/

82

trigger(name: string, payload?: any): void;

83

}

84

```

85

86

**Usage Examples:**

87

88

```typescript

89

import { EventBus } from "@odoo/owl";

90

91

// Global event bus

92

const globalEventBus = new EventBus();

93

94

// Component communication

95

class NotificationService {

96

constructor() {

97

this.eventBus = new EventBus();

98

}

99

100

show(message, type = "info") {

101

this.eventBus.trigger("notification:show", { message, type, id: Date.now() });

102

}

103

104

hide(id) {

105

this.eventBus.trigger("notification:hide", { id });

106

}

107

}

108

109

class NotificationComponent extends Component {

110

setup() {

111

this.notifications = useState([]);

112

113

// Subscribe to notification events

114

notificationService.eventBus.addEventListener("notification:show", (event) => {

115

this.notifications.push(event.detail);

116

// Auto-hide after delay

117

setTimeout(() => {

118

this.hideNotification(event.detail.id);

119

}, 5000);

120

});

121

122

notificationService.eventBus.addEventListener("notification:hide", (event) => {

123

this.hideNotification(event.detail.id);

124

});

125

}

126

127

hideNotification(id) {

128

const index = this.notifications.findIndex(n => n.id === id);

129

if (index >= 0) {

130

this.notifications.splice(index, 1);

131

}

132

}

133

}

134

135

// Global state management

136

class UserService {

137

constructor() {

138

this.eventBus = new EventBus();

139

this.currentUser = null;

140

}

141

142

login(user) {

143

this.currentUser = user;

144

this.eventBus.trigger("user:login", user);

145

}

146

147

logout() {

148

const user = this.currentUser;

149

this.currentUser = null;

150

this.eventBus.trigger("user:logout", user);

151

}

152

}

153

154

// Multiple components can listen to user events

155

class HeaderComponent extends Component {

156

setup() {

157

userService.eventBus.addEventListener("user:login", (event) => {

158

this.render(); // Re-render header with user info

159

});

160

161

userService.eventBus.addEventListener("user:logout", (event) => {

162

this.render(); // Re-render header without user info

163

});

164

}

165

}

166

167

// Cleanup subscriptions

168

class TemporaryComponent extends Component {

169

setup() {

170

this.handleUserLogin = (event) => {

171

console.log("User logged in:", event.detail);

172

};

173

174

globalEventBus.addEventListener("user:login", this.handleUserLogin);

175

176

onWillDestroy(() => {

177

// Clean up subscriptions

178

globalEventBus.removeEventListener("user:login", this.handleUserLogin);

179

});

180

}

181

}

182

```

183

184

#### htmlEscape

185

186

Escapes HTML special characters to prevent XSS attacks.

187

188

```typescript { .api }

189

/**

190

* Escapes HTML special characters

191

* @param str - String to escape

192

* @returns HTML-escaped string

193

*/

194

function htmlEscape(str: string): string;

195

```

196

197

**Usage Examples:**

198

199

```typescript

200

import { htmlEscape } from "@odoo/owl";

201

202

// Escape user input

203

const userInput = '<script>alert("XSS")</script>';

204

const safeHTML = htmlEscape(userInput);

205

console.log(safeHTML); // &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

206

207

// Safe display of user content

208

class CommentComponent extends Component {

209

static template = xml`

210

<div class="comment">

211

<h4><t t-esc="props.comment.author" /></h4>

212

<div t-raw="safeContent" />

213

<small><t t-esc="formattedDate" /></small>

214

</div>

215

`;

216

217

get safeContent() {

218

// Escape HTML but allow some basic formatting

219

let content = htmlEscape(this.props.comment.content);

220

// Optionally allow some safe HTML after escaping

221

content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');

222

content = content.replace(/\*(.*?)\*/g, '<em>$1</em>');

223

return content;

224

}

225

226

get formattedDate() {

227

return new Date(this.props.comment.createdAt).toLocaleDateString();

228

}

229

}

230

231

// Form validation with safe error display

232

class FormComponent extends Component {

233

validateAndShowError(input, errorContainer) {

234

const value = input.value;

235

let error = null;

236

237

if (value.length < 3) {

238

error = "Must be at least 3 characters";

239

} else if (value.includes("<script>")) {

240

error = "Invalid characters detected";

241

}

242

243

if (error) {

244

// Safely display error (though this is a controlled string)

245

errorContainer.textContent = htmlEscape(error);

246

errorContainer.style.display = "block";

247

return false;

248

} else {

249

errorContainer.style.display = "none";

250

return true;

251

}

252

}

253

}

254

```

255

256

#### whenReady

257

258

Executes a callback when the DOM is ready.

259

260

```typescript { .api }

261

/**

262

* Executes callback when DOM is ready

263

* @param callback - Function to execute when DOM is ready

264

*/

265

function whenReady(callback: () => void): void;

266

```

267

268

**Usage Examples:**

269

270

```typescript

271

import { whenReady, mount, Component, xml } from "@odoo/owl";

272

273

// Wait for DOM before mounting app

274

class App extends Component {

275

static template = xml`<div>App is ready!</div>`;

276

}

277

278

whenReady(() => {

279

mount(App, document.body);

280

});

281

282

// Initialize scripts after DOM is ready

283

whenReady(() => {

284

// Initialize third-party libraries

285

initAnalytics();

286

setupGlobalErrorHandling();

287

loadUserPreferences();

288

289

// Setup global event listeners

290

document.addEventListener("keydown", handleGlobalKeyDown);

291

window.addEventListener("beforeunload", handleBeforeUnload);

292

});

293

294

// Conditional initialization

295

whenReady(() => {

296

if (document.getElementById("owl-app")) {

297

// Mount OWL app

298

mount(MainApp, document.getElementById("owl-app"));

299

}

300

301

if (document.getElementById("legacy-widget")) {

302

// Initialize legacy jQuery widget

303

$("#legacy-widget").widget();

304

}

305

});

306

```

307

308

#### loadFile

309

310

Loads a file from a URL asynchronously.

311

312

```typescript { .api }

313

/**

314

* Loads a file from URL

315

* @param url - URL to load

316

* @returns Promise resolving to file content as string

317

*/

318

function loadFile(url: string): Promise<string>;

319

```

320

321

**Usage Examples:**

322

323

```typescript

324

import { loadFile, Component, xml } from "@odoo/owl";

325

326

// Load template files

327

class TemplateLoader extends Component {

328

setup() {

329

this.state = useState({

330

template: null,

331

loading: true,

332

error: null

333

});

334

335

onWillStart(async () => {

336

try {

337

const templateContent = await loadFile("/templates/custom-template.xml");

338

this.state.template = templateContent;

339

} catch (error) {

340

this.state.error = "Failed to load template";

341

} finally {

342

this.state.loading = false;

343

}

344

});

345

}

346

}

347

348

// Load configuration files

349

class ConfigurableComponent extends Component {

350

setup() {

351

this.config = null;

352

353

onWillStart(async () => {

354

try {

355

const configJSON = await loadFile("/config/app-config.json");

356

this.config = JSON.parse(configJSON);

357

} catch (error) {

358

// Use default config

359

this.config = { theme: "light", lang: "en" };

360

}

361

});

362

}

363

}

364

365

// Load CSS dynamically

366

async function loadTheme(themeName) {

367

try {

368

const cssContent = await loadFile(`/themes/${themeName}.css`);

369

370

// Inject CSS into page

371

const style = document.createElement("style");

372

style.textContent = cssContent;

373

document.head.appendChild(style);

374

375

return true;

376

} catch (error) {

377

console.error("Failed to load theme:", error);

378

return false;

379

}

380

}

381

382

// Load and parse data files

383

class DataViewer extends Component {

384

setup() {

385

this.state = useState({ data: null, loading: true });

386

387

onWillStart(async () => {

388

try {

389

const csvContent = await loadFile("/data/sample-data.csv");

390

this.state.data = this.parseCSV(csvContent);

391

} catch (error) {

392

this.state.data = [];

393

} finally {

394

this.state.loading = false;

395

}

396

});

397

}

398

399

parseCSV(csvText) {

400

const lines = csvText.split('\n');

401

const headers = lines[0].split(',');

402

403

return lines.slice(1).map(line => {

404

const values = line.split(',');

405

return headers.reduce((obj, header, index) => {

406

obj[header.trim()] = values[index]?.trim() || '';

407

return obj;

408

}, {});

409

});

410

}

411

}

412

```

413

414

#### markup

415

416

Template literal tag for creating markup strings.

417

418

```typescript { .api }

419

/**

420

* Template literal tag for markup strings

421

* @param template - Template string parts

422

* @param args - Interpolated values

423

* @returns Markup string

424

*/

425

function markup(template: TemplateStringsArray, ...args: any[]): string;

426

```

427

428

**Usage Examples:**

429

430

```typescript

431

import { markup, htmlEscape } from "@odoo/owl";

432

433

// Safe markup creation

434

const createUserCard = (user) => {

435

const safeUserName = htmlEscape(user.name);

436

const safeEmail = htmlEscape(user.email);

437

438

return markup`

439

<div class="user-card" data-user-id="${user.id}">

440

<h3>${safeUserName}</h3>

441

<p>${safeEmail}</p>

442

<button onclick="editUser(${user.id})">Edit</button>

443

</div>

444

`;

445

};

446

447

// Dynamic content generation

448

const createTable = (data, columns) => {

449

const headerRow = markup`

450

<tr>

451

${columns.map(col => `<th>${htmlEscape(col.title)}</th>`).join('')}

452

</tr>

453

`;

454

455

const dataRows = data.map(row => {

456

const cells = columns.map(col => {

457

const value = row[col.field] || '';

458

return `<td>${htmlEscape(String(value))}</td>`;

459

}).join('');

460

461

return markup`<tr>${cells}</tr>`;

462

}).join('');

463

464

return markup`

465

<table class="data-table">

466

<thead>${headerRow}</thead>

467

<tbody>${dataRows}</tbody>

468

</table>

469

`;

470

};

471

472

// Email template generation

473

const createEmailTemplate = (user, data) => {

474

return markup`

475

<!DOCTYPE html>

476

<html>

477

<head>

478

<title>Welcome ${htmlEscape(user.name)}</title>

479

</head>

480

<body>

481

<h1>Welcome, ${htmlEscape(user.name)}!</h1>

482

<p>Thank you for joining our platform.</p>

483

<ul>

484

${data.features.map(feature =>

485

`<li>${htmlEscape(feature)}</li>`

486

).join('')}

487

</ul>

488

<p>Best regards,<br>The Team</p>

489

</body>

490

</html>

491

`;

492

};

493

```

494

495

### Validation System

496

497

#### validate

498

499

Validates a value against a schema with detailed error reporting.

500

501

```typescript { .api }

502

/**

503

* Validates an object against a schema

504

* @param obj - Object to validate

505

* @param spec - Schema specification

506

* @throws OwlError if validation fails

507

*/

508

function validate(obj: { [key: string]: any }, spec: Schema): void;

509

510

/**

511

* Helper validation function that returns list of errors

512

* @param obj - Object to validate

513

* @param schema - Schema specification

514

* @returns Array of error messages

515

*/

516

function validateSchema(obj: { [key: string]: any }, schema: Schema): string[];

517

```

518

519

**Usage Examples:**

520

521

```typescript

522

import { validate, Component } from "@odoo/owl";

523

524

// Component props validation

525

class UserProfile extends Component {

526

static props = {

527

user: {

528

type: Object,

529

shape: {

530

id: Number,

531

name: String,

532

email: String,

533

age: { type: Number, optional: true }

534

}

535

},

536

showEmail: { type: Boolean, optional: true }

537

};

538

539

setup() {

540

// Manual validation in development

541

if (this.env.dev) {

542

try {

543

validate("user", this.props.user, UserProfile.props.user);

544

console.log("Props validation passed");

545

} catch (error) {

546

console.error("Props validation failed:", error.message);

547

}

548

}

549

}

550

}

551

552

// Form validation

553

class ContactForm extends Component {

554

validateForm() {

555

const formData = {

556

name: this.refs.nameInput.value,

557

email: this.refs.emailInput.value,

558

age: parseInt(this.refs.ageInput.value) || null,

559

newsletter: this.refs.newsletterCheckbox.checked

560

};

561

562

const schema = {

563

name: String,

564

email: String,

565

age: { type: Number, optional: true },

566

newsletter: Boolean

567

};

568

569

try {

570

validate("contactForm", formData, schema);

571

this.submitForm(formData);

572

return true;

573

} catch (error) {

574

this.showError(`Validation failed: ${error.message}`);

575

return false;

576

}

577

}

578

}

579

580

// API response validation

581

class DataService {

582

async fetchUser(userId) {

583

const response = await fetch(`/api/users/${userId}`);

584

const userData = await response.json();

585

586

// Validate API response structure

587

const userSchema = {

588

id: Number,

589

name: String,

590

email: String,

591

profile: {

592

type: Object,

593

optional: true,

594

shape: {

595

avatar: { type: String, optional: true },

596

bio: { type: String, optional: true }

597

}

598

},

599

permissions: {

600

type: Array,

601

element: String

602

}

603

};

604

605

try {

606

validate("apiUser", userData, userSchema);

607

return userData;

608

} catch (error) {

609

throw new Error(`Invalid user data from API: ${error.message}`);

610

}

611

}

612

}

613

```

614

615

#### validateType

616

617

Validates a value against a type description.

618

619

```typescript { .api }

620

/**

621

* Validates a value against a type description

622

* @param key - Key name for error messages

623

* @param value - Value to validate

624

* @param descr - Type description to validate against

625

* @returns Error message string or null if valid

626

*/

627

function validateType(key: string, value: any, descr: TypeDescription): string | null;

628

```

629

630

**Usage Examples:**

631

632

```typescript

633

import { validateType } from "@odoo/owl";

634

635

// Basic type checking

636

console.log(validateType("hello", String)); // true

637

console.log(validateType(42, Number)); // true

638

console.log(validateType(true, Boolean)); // true

639

console.log(validateType([], Array)); // true

640

console.log(validateType({}, Object)); // true

641

642

// Complex type validation

643

const userType = {

644

type: Object,

645

shape: {

646

id: Number,

647

name: String,

648

active: Boolean

649

}

650

};

651

652

const validUser = { id: 1, name: "John", active: true };

653

const invalidUser = { id: "1", name: "John", active: "yes" };

654

655

console.log(validateType(validUser, userType)); // true

656

console.log(validateType(invalidUser, userType)); // false

657

658

// Array validation

659

const numberArrayType = {

660

type: Array,

661

element: Number

662

};

663

664

console.log(validateType([1, 2, 3], numberArrayType)); // true

665

console.log(validateType([1, "2", 3], numberArrayType)); // false

666

667

// Optional fields

668

const optionalFieldType = {

669

type: Object,

670

shape: {

671

required: String,

672

optional: { type: Number, optional: true }

673

}

674

};

675

676

console.log(validateType({ required: "yes" }, optionalFieldType)); // true

677

console.log(validateType({ required: "yes", optional: 42 }, optionalFieldType)); // true

678

console.log(validateType({ optional: 42 }, optionalFieldType)); // false (missing required)

679

680

// Union types

681

const unionType = [String, Number];

682

console.log(validateType("hello", unionType)); // true

683

console.log(validateType(42, unionType)); // true

684

console.log(validateType(true, unionType)); // false

685

686

// Dynamic type checking

687

class FormField extends Component {

688

validateInput(value) {

689

let type;

690

691

switch (this.props.fieldType) {

692

case "text":

693

type = String;

694

break;

695

case "number":

696

type = Number;

697

break;

698

case "email":

699

type = { type: String, validate: (v) => v.includes("@") };

700

break;

701

default:

702

type = true; // Accept any type

703

}

704

705

return validateType(value, type);

706

}

707

}

708

```

709

710

### Schema Types

711

712

```typescript { .api }

713

/**

714

* Validation schema types

715

*/

716

type Schema = string[] | { [key: string]: TypeDescription };

717

718

type TypeDescription = BaseType | TypeInfo | ValueType | TypeDescription[];

719

720

type BaseType = { new (...args: any[]): any } | true | "*";

721

722

interface TypeInfo {

723

/** Type to validate against */

724

type?: TypeDescription;

725

/** Whether the field is optional */

726

optional?: boolean;

727

/** Custom validation function */

728

validate?: (value: any) => boolean;

729

/** Object shape for Object types */

730

shape?: Schema;

731

/** Element type for Array types */

732

element?: TypeDescription;

733

/** Value type for Map-like objects */

734

values?: TypeDescription;

735

}

736

737

interface ValueType {

738

/** Exact value to match */

739

value: any;

740

}

741

```

742

743

### Validation Examples

744

745

```typescript

746

// Component with comprehensive validation

747

class ProductForm extends Component {

748

static props = {

749

product: {

750

type: Object,

751

shape: {

752

id: { type: Number, optional: true },

753

name: String,

754

price: Number,

755

category: ["electronics", "books", "clothing"], // Union of specific values

756

tags: { type: Array, element: String },

757

metadata: {

758

type: Object,

759

optional: true,

760

shape: {

761

weight: { type: Number, optional: true },

762

dimensions: {

763

type: Object,

764

optional: true,

765

shape: {

766

width: Number,

767

height: Number,

768

depth: Number

769

}

770

}

771

}

772

}

773

}

774

},

775

onSave: Function,

776

readonly: { type: Boolean, optional: true }

777

};

778

}

779

```