Compare commits

..

2 Commits

Author SHA1 Message Date
Xavier Morel 9edd31f755 [ADD] runtime/utils: optional validation to EventBus
Currently the event bus allows sending and listening to arbitrary
events, I got got by that when I pushed a fix using `addEventListener`
on a bus across an events renaming, and on the other side the fix did
nothing anymore. Entirely my fault, but if the list of events sent on
a bus is known and documented (e.g. a jsdoc has `@emits` tags) it
would make sense for both the listening and the dispatching to also be
validated.

This proposal performs validation only when the eventbus is created:

- in dev mode (which also requires being in a component context)
- if an iterable of events is passed to the ctor

The dev-mode check might be overkill but it seems like a good idea at
least for an initial version, as the validation does have a cost
however low, and validation errors can occur essentially anywhere.
2025-09-12 14:09:38 +02:00
Damien Bouvy c2728c9daf [DOC] slots prop validation
Make it explicit that props validation should accept `slots` if a component uses slots (even the `default` slot).
2025-09-03 13:46:45 +02:00
3 changed files with 128 additions and 1 deletions
+22
View File
@@ -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,
+43
View File
@@ -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) {
+63 -1
View File
@@ -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", () => {