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); // <script>alert("XSS")</script>
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
```