mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9edd31f755 | |||
| c2728c9daf | |||
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 |
@@ -320,6 +320,28 @@ class ComponentB extends owl.Component {
|
||||
|
||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||
|
||||
### `slots` prop
|
||||
|
||||
If a component that uses [slots](slots.md) also lists or validates its props, then
|
||||
you will have to explicitely allow the `slots` prop (with an `Object` type), or
|
||||
allow extra props using the `*` notation mentioned above. This is because slots
|
||||
are provided to a component [as props](slots.md#slots-and-props).
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
class MyComponent extends Component {
|
||||
static props = [someProp, slots?];
|
||||
}
|
||||
|
||||
class MyComponentWithValidation extends Component {
|
||||
static props = {
|
||||
someProp: {type: Number, optional: true},
|
||||
slots : {type: Object, optional: true},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Good Practices
|
||||
|
||||
A `props` object is a collection of values that come from the parent. As such,
|
||||
|
||||
+15
-6
@@ -3850,7 +3850,16 @@ class CodeTarget {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
const TRANSLATABLE_ATTRS = [
|
||||
"alt",
|
||||
"aria-label",
|
||||
"aria-placeholder",
|
||||
"aria-roledescription",
|
||||
"aria-valuetext",
|
||||
"label",
|
||||
"placeholder",
|
||||
"title",
|
||||
];
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
class CodeGenerator {
|
||||
constructor(ast, options) {
|
||||
@@ -4968,11 +4977,11 @@ function parseNode(node, ctx) {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -5758,7 +5767,7 @@ function compile(template, options = {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.7.0";
|
||||
const version = "2.8.0";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -6229,6 +6238,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2025-03-26T12:58:40.935Z';
|
||||
__info__.hash = 'e788e36';
|
||||
__info__.date = '2025-06-30T12:46:06.424Z';
|
||||
__info__.hash = 'b620502';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -254,7 +254,16 @@ class CodeTarget {
|
||||
}
|
||||
}
|
||||
|
||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
||||
const TRANSLATABLE_ATTRS = [
|
||||
"alt",
|
||||
"aria-label",
|
||||
"aria-placeholder",
|
||||
"aria-roledescription",
|
||||
"aria-valuetext",
|
||||
"label",
|
||||
"placeholder",
|
||||
"title",
|
||||
];
|
||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||
|
||||
export class CodeGenerator {
|
||||
|
||||
@@ -253,11 +253,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTPortal(node, ctx) ||
|
||||
parseTCall(node, ctx) ||
|
||||
parseTCallBlock(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTEscNode(node, ctx) ||
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { ComponentNode, getCurrent } from "./component_node";
|
||||
export type Callback = () => void;
|
||||
|
||||
/**
|
||||
@@ -81,10 +82,52 @@ export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||
}
|
||||
|
||||
export class EventBus extends EventTarget {
|
||||
constructor(events?: string[]) {
|
||||
if (events) {
|
||||
let node: ComponentNode | null = null;
|
||||
try {
|
||||
node = getCurrent();
|
||||
} catch {}
|
||||
if (node?.app?.dev) {
|
||||
return new DebugEventBus(events);
|
||||
}
|
||||
}
|
||||
super();
|
||||
}
|
||||
trigger(name: string, payload?: any) {
|
||||
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
|
||||
}
|
||||
}
|
||||
class DebugEventBus extends EventBus {
|
||||
private events: Set<string>;
|
||||
constructor(events: string[]) {
|
||||
super();
|
||||
this.events = new Set(events);
|
||||
}
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject | null,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void {
|
||||
if (!this.events.has(type)) {
|
||||
throw new OwlError(`EventBus: subscribing to unknown event '${type}'`);
|
||||
}
|
||||
super.addEventListener(type, listener, options);
|
||||
}
|
||||
trigger(name: string, payload?: any) {
|
||||
if (!this.events.has(name)) {
|
||||
throw new OwlError(`EventBus: triggering unknown event '${name}'`);
|
||||
}
|
||||
super.trigger(name, payload);
|
||||
}
|
||||
|
||||
dispatchEvent(event: Event): boolean {
|
||||
if (!this.events.has(event.type)) {
|
||||
throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`);
|
||||
}
|
||||
return super.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
export function whenReady(fn?: any): Promise<void> {
|
||||
return new Promise(function (resolve) {
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.7.0";
|
||||
export const version = "2.8.0";
|
||||
|
||||
@@ -1971,6 +1971,54 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('t-translation="off": interaction with t-esc', async () => {
|
||||
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
|
||||
type: ASTType.TTranslation,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
defaultValue: "",
|
||||
expr: "a",
|
||||
type: ASTType.TEsc,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('t-translation="off": interaction with t-out', async () => {
|
||||
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
|
||||
type: ASTType.TTranslation,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
body: null,
|
||||
expr: "a",
|
||||
type: ASTType.TOut,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2008,6 +2056,56 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("t-translation-context: interaction with t-esc", async () => {
|
||||
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
|
||||
type: ASTType.TTranslationContext,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
defaultValue: "",
|
||||
expr: "a",
|
||||
type: ASTType.TEsc,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
});
|
||||
});
|
||||
|
||||
test("t-translation-context: interaction with t-out", async () => {
|
||||
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
|
||||
type: ASTType.TTranslationContext,
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
body: null,
|
||||
expr: "a",
|
||||
type: ASTType.TOut,
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "span",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context-attr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+63
-1
@@ -1,5 +1,7 @@
|
||||
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||
import { nextMicroTick } from "./helpers";
|
||||
import { makeTestFixture, nextMicroTick } from "./helpers";
|
||||
import { getCurrent } from "../src/runtime/component_node";
|
||||
import { Component, mount, xml } from "../src";
|
||||
|
||||
describe("event bus behaviour", () => {
|
||||
test("can subscribe and be notified", () => {
|
||||
@@ -33,6 +35,66 @@ describe("event bus behaviour", () => {
|
||||
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
|
||||
bus.trigger("event", "hello world");
|
||||
});
|
||||
|
||||
test("events are not validated if the bus is created outside of dev mode", async () => {
|
||||
let bus_empty: EventBus | null = null;
|
||||
class Root extends Component {
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
getCurrent(); // checks that we're in a component context
|
||||
|
||||
bus_empty = new EventBus([]);
|
||||
}
|
||||
}
|
||||
await mount(Root, makeTestFixture());
|
||||
|
||||
bus_empty!.addEventListener("a", () => {});
|
||||
bus_empty!.trigger("a");
|
||||
bus_empty!.dispatchEvent(new CustomEvent("a"));
|
||||
});
|
||||
test("events are validated if the bus is created in dev mode & events are provided", async () => {
|
||||
let bus: EventBus | null = null;
|
||||
let bus_empty: EventBus | null = null;
|
||||
let bbus_no_validation: EventBus | null = null;
|
||||
class Root extends Component {
|
||||
static template = xml`<div/>`;
|
||||
|
||||
setup() {
|
||||
getCurrent(); // checks that we're in a component context
|
||||
|
||||
bus = new EventBus(["a", "b"]);
|
||||
bus_empty = new EventBus([]);
|
||||
bbus_no_validation = new EventBus();
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Root, makeTestFixture(), { test: true });
|
||||
|
||||
bbus_no_validation!.addEventListener("c", () => {});
|
||||
bbus_no_validation!.trigger("c");
|
||||
bbus_no_validation!.dispatchEvent(new CustomEvent("c"));
|
||||
|
||||
bus!.addEventListener("a", () => {});
|
||||
bus!.trigger("a");
|
||||
bus!.dispatchEvent(new CustomEvent("a"));
|
||||
|
||||
expect(() => bus!.addEventListener("c", () => {})).toThrow(
|
||||
"EventBus: subscribing to unknown event 'c'"
|
||||
);
|
||||
expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'");
|
||||
expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow(
|
||||
"EventBus: dispatching unknown event 'c'"
|
||||
);
|
||||
|
||||
expect(() => bus_empty!.addEventListener("a", () => {})).toThrow(
|
||||
"EventBus: subscribing to unknown event 'a'"
|
||||
);
|
||||
expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'");
|
||||
expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow(
|
||||
"EventBus: dispatching unknown event 'a'"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("batched", () => {
|
||||
|
||||
Reference in New Issue
Block a user