Skip to content

Commit 9c201fc

Browse files
committed
add forward-custom-events attribute
1 parent 6590bf6 commit 9c201fc

File tree

7 files changed

+345
-95
lines changed

7 files changed

+345
-95
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {expect} from '@esm-bundle/chai';
2+
import {ComponentContext} from '@spearwolf/shadow-objects';
3+
import '@spearwolf/shadow-objects/shae-ent.js';
4+
import '@spearwolf/shadow-objects/shae-worker.js';
5+
import {findElementsById} from '../src/findElementsById.js';
6+
import {render} from '../src/render.js';
7+
8+
describe('forward-custom-events', () => {
9+
beforeEach(async () => {
10+
render(`
11+
<shae-worker local no-autostart auto-sync="no" id="localEnv" no-structured-clone></shae-worker>
12+
13+
<shae-ent id="a" token="a" forward-custom-events></shae-ent>
14+
<shae-ent id="b" token="b" forward-custom-events="foo"></shae-ent>
15+
<shae-ent id="c" token="c" forward-custom-events="foo, bar"></shae-ent>
16+
`);
17+
18+
await Promise.all(['shae-worker', 'shae-ent'].map((name) => customElements.whenDefined(name)));
19+
});
20+
21+
afterEach(() => {
22+
ComponentContext.get().clear();
23+
document.getElementById('localEnv').shadowEnv.destroy();
24+
});
25+
26+
it('should forward events as CustomEvents', async () => {
27+
const [a, b, c, localEnv] = findElementsById('a', 'b', 'c', 'localEnv');
28+
29+
await localEnv.start();
30+
31+
const eventsA = [];
32+
const eventsB = [];
33+
const eventsC = [];
34+
35+
a.addEventListener('foo', (e) => eventsA.push(e));
36+
a.addEventListener('bar', (e) => eventsA.push(e));
37+
a.addEventListener('baz', (e) => eventsA.push(e));
38+
39+
b.addEventListener('foo', (e) => eventsB.push(e));
40+
b.addEventListener('bar', (e) => eventsB.push(e));
41+
b.addEventListener('baz', (e) => eventsB.push(e));
42+
43+
c.addEventListener('foo', (e) => eventsC.push(e));
44+
c.addEventListener('bar', (e) => eventsC.push(e));
45+
c.addEventListener('baz', (e) => eventsC.push(e));
46+
47+
// Simulate events arriving at the ViewComponent (from Shadow Objects)
48+
a.viewComponent.dispatchEvent('foo', {val: 'a-foo'}, false);
49+
a.viewComponent.dispatchEvent('bar', {val: 'a-bar'}, false);
50+
a.viewComponent.dispatchEvent('baz', {val: 'a-baz'}, false);
51+
52+
b.viewComponent.dispatchEvent('foo', {val: 'b-foo'}, false);
53+
b.viewComponent.dispatchEvent('bar', {val: 'b-bar'}, false);
54+
b.viewComponent.dispatchEvent('baz', {val: 'b-baz'}, false);
55+
56+
c.viewComponent.dispatchEvent('foo', {val: 'c-foo'}, false);
57+
c.viewComponent.dispatchEvent('bar', {val: 'c-bar'}, false);
58+
c.viewComponent.dispatchEvent('baz', {val: 'c-baz'}, false);
59+
60+
// Events are synchronous when dispatched via dispatchEvent/emit locally?
61+
// The on(...) subscription is synchronous.
62+
// The CustomEvent dispatch is synchronous.
63+
// So we don't need to wait for syncWait() unless there's an async process involved.
64+
// But let's check.
65+
66+
// Check A (all events forwarded)
67+
expect(eventsA).to.have.length(3);
68+
expect(eventsA[0].type).to.equal('foo');
69+
expect(eventsA[0].detail).to.deep.equal({val: 'a-foo'});
70+
expect(eventsA[1].type).to.equal('bar');
71+
expect(eventsA[1].detail).to.deep.equal({val: 'a-bar'});
72+
expect(eventsA[2].type).to.equal('baz');
73+
expect(eventsA[2].detail).to.deep.equal({val: 'a-baz'});
74+
75+
// Check B (only 'foo' forwarded)
76+
expect(eventsB).to.have.length(1);
77+
expect(eventsB[0].type).to.equal('foo');
78+
expect(eventsB[0].detail).to.deep.equal({val: 'b-foo'});
79+
80+
// Check C (only 'foo' and 'bar' forwarded)
81+
expect(eventsC).to.have.length(2);
82+
expect(eventsC[0].type).to.equal('foo');
83+
expect(eventsC[0].detail).to.deep.equal({val: 'c-foo'});
84+
expect(eventsC[1].type).to.equal('bar');
85+
expect(eventsC[1].detail).to.deep.equal({val: 'c-bar'});
86+
});
87+
});

packages/shadow-objects/CHANGELOG.md

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,55 @@ All notable changes to [@spearwolf/shadow-objects](https://github.com/spearwolf/
55
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.29.0] - 2026-01-21
9+
10+
- **New Feature:** Added `forward-custom-events` attribute to `<shae-ent>` custom element.
11+
- Allows forwarding events emitted by the internal `ViewComponent` (Shadow Object) as standard DOM `CustomEvent`s on the `<shae-ent>` element.
12+
- Supports forwarding all events or filtering specific event types (e.g., `forward-custom-events="my-event,another-event"`).
13+
- Event payload is passed as `detail` property of the `CustomEvent`.
14+
815
## [0.28.0] - 2026-01-20
916

1017
- **API Update:** `on()` and `once()` in `ShadowObjectCreationAPI` now support an implicit event source.
11-
- If the first argument is a `string`, `symbol`, or `[]`, the `entity` is automatically used as the event source.
12-
- Example: `on('eventName', callback)` is equivalent to `on(entity, 'eventName', callback)`.
13-
- This simplifies the common case of listening to entity events.
18+
- If the first argument is a `string`, `symbol`, or `[]`, the `entity` is automatically used as the event source.
19+
- Example: `on('eventName', callback)` is equivalent to `on(entity, 'eventName', callback)`.
20+
- This simplifies the common case of listening to entity events.
1421
- **API Update:** introduce `onViewEvent()` in `ShadowObjectCreationAPI`
15-
- Simplifies listening to view events dispatched to the entity.
16-
- Example:
17-
```typescript
18-
onViewEvent((type, data) => {
19-
if (type === 'my-event') {
20-
// handle event
21-
}
22-
});
23-
```
22+
- Simplifies listening to view events dispatched to the entity.
23+
- Example:
24+
```typescript
25+
onViewEvent((type, data) => {
26+
if (type === 'my-event') {
27+
// handle event
28+
}
29+
});
30+
```
2431
- **Refactor** the `EntityApi` type
2532
- **Refactor** the `useProperties` supports type maps now
2633
- **Documentation:** Comprehensive update to the documentation structure and content.
2734

2835
### ⚠️ Breaking Changes
36+
2937
- The _entity_ events `onCreate`, `onDestroy`, `onParentChanged` and `onViewEvent` changed to _symbols_.
30-
- Update your event listeners accordingly:
31-
- import the event symbols from the package:
32-
```typescript
33-
import { onCreate, onDestroy, onParentChanged, onViewEvent } from '@spearwolf/shadow-objects/shadow-objects.js';
34-
```
35-
- _Functional Shadow-Objects:_
36-
- **Before:** `on(entity, 'onCreate', ...)`
37-
- **After:** `on(onCreate, ...)`
38-
- _Class-based Shadow-Objects:_
39-
- **Before:** `onCreate(entity)`
40-
- **After:** `[onCreate](entity)`
38+
- Update your event listeners accordingly:
39+
- import the event symbols from the package:
40+
```typescript
41+
import {onCreate, onDestroy, onParentChanged, onViewEvent} from '@spearwolf/shadow-objects/shadow-objects.js';
42+
```
43+
- _Functional Shadow-Objects:_
44+
- **Before:** `on(entity, 'onCreate', ...)`
45+
- **After:** `on(onCreate, ...)`
46+
- _Class-based Shadow-Objects:_
47+
- **Before:** `onCreate(entity)`
48+
- **After:** `[onCreate](entity)`
4149

4250
## [0.27.0] - 2026-01-19
4351

4452
### ⚠️ Breaking Changes
4553

4654
- **API Update:** `dispatchMessageToView` has been moved from the `entity` instance to the `ShadowObjectCreationAPI`.
47-
- **Before:** `entity.dispatchMessageToView(...)`
48-
- **After:** `dispatchMessageToView(...)` (available as an argument in the constructor/factory function)
55+
- **Before:** `entity.dispatchMessageToView(...)`
56+
- **After:** `dispatchMessageToView(...)` (available as an argument in the constructor/factory function)
4957
- **Type Definitions:** Removed `dispatchMessageToView` from `EntityApi` interface.
5058

5159
## [0.26.4] - 2026-01-15

packages/shadow-objects/docs/01-concepts/04-entity-tree-context-events.md

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,35 @@ This section explores the hierarchical nature of the Shadow World, explaining ho
99
At the core of the Shadow World is the **Entity Tree**. This tree structure mirrors the hierarchy of your View components (e.g., the DOM structure of `<shae-ent>` elements).
1010

1111
### Hierarchy
12-
* **Entities (Puppets):** These are the nodes in the tree. Every `<shae-ent>` in your HTML corresponds to one Entity instance in the Shadow World.
13-
* **Shadow Objects (Logic):** Shadow Objects are **attached** to these Entities.
12+
13+
- **Entities (Puppets):** These are the nodes in the tree. Every `<shae-ent>` in your HTML corresponds to one Entity instance in the Shadow World.
14+
- **Shadow Objects (Logic):** Shadow Objects are **attached** to these Entities.
1415

1516
> [!IMPORTANT]
1617
> **Shadow Objects are NOT nodes in the tree.**
1718
> They are "components" or "behaviors" attached to an Entity node.
1819
>
19-
> * An Entity can have multiple Shadow Objects (via Routing).
20-
> * All Shadow Objects on the same Entity share the same properties and lifecycle.
20+
> - An Entity can have multiple Shadow Objects (via Routing).
21+
> - All Shadow Objects on the same Entity share the same properties and lifecycle.
2122
2223
## Context (Dependency Injection)
2324

2425
Context allows you to share data deep into the component tree without passing props manually at every level ("prop drilling").
2526

2627
### How it Works
28+
2729
1. **Provider:** A Shadow Object on an Entity calls `provideContext`.
2830
2. **Scope:** This value becomes available to:
29-
* **All other Shadow Objects on the same Entity.**
30-
* **All Shadow Objects on descendant (child) Entities.**
31+
- **All other Shadow Objects on the same Entity.**
32+
- **All Shadow Objects on descendant (child) Entities.**
3133
3. **Consumer:** A Shadow Object calls `useContext` to read the value.
3234

3335
### Context is Entity-Bound
36+
3437
Since Context is attached to the **Entity**, it acts as a shared bus for all logic attached to that node.
3538

36-
* If `ShadowObject A` provides a context, `ShadowObject B` (on the same Entity) can immediately consume it.
37-
* This is the primary way to compose complex logic from smaller, reusable Shadow Objects.
39+
- If `ShadowObject A` provides a context, `ShadowObject B` (on the same Entity) can immediately consume it.
40+
- This is the primary way to compose complex logic from smaller, reusable Shadow Objects.
3841

3942
```mermaid
4043
graph TD
@@ -51,70 +54,78 @@ graph TD
5154
```
5255

5356
### Reactivity
57+
5458
Context values are **Signals**.
55-
* If the Provider updates the value, all Consumers (even deep in the tree) update automatically.
56-
* You don't need to subscribe manually; just reading the value in an effect creates a dependency.
59+
60+
- If the Provider updates the value, all Consumers (even deep in the tree) update automatically.
61+
- You don't need to subscribe manually; just reading the value in an effect creates a dependency.
5762

5863
## Events
5964

6065
The framework provides a powerful event system based on [@spearwolf/eventize](https://github.com/spearwolf/eventize). This allows decoupled communication between Shadow Objects, Entities, and the View.
6166

6267
### The Event Bus: The Entity
68+
6369
Every Entity acts as an event emitter. Since multiple Shadow Objects can be attached to the same Entity, the Entity serves as a shared "bus" for them.
6470

65-
* **Shared Scope:** If one Shadow Object emits an event on its Entity, **all other Shadow Objects on that same Entity** can receive it.
66-
* **Decoupling:** Shadow Objects don't need to know about each other; they just listen to the Entity they are attached to.
71+
- **Shared Scope:** If one Shadow Object emits an event on its Entity, **all other Shadow Objects on that same Entity** can receive it.
72+
- **Decoupling:** Shadow Objects don't need to know about each other; they just listen to the Entity they are attached to.
6773

6874
### 1. View -> Shadow Events
75+
6976
Events dispatched by the View Component are automatically forwarded to the corresponding Entity as _View Events_.
7077

71-
* **View Layer:**
72-
```javascript
73-
// In your Web Component or View Logic
74-
this.viewComponent.dispatchShadowObjectsEvent('my-custom-event', { some: 'data' });
75-
```
76-
* **Shadow World:**
77-
```typescript
78-
// Your Shadow Object
79-
export function MyLogic({ onViewEvent }: ShadowObjectCreationAPI) {
80-
onViewEvent((type, data) => {
81-
if (type === 'my-custom-event') {
82-
console.log('Received from View:', data);
83-
}
84-
});
85-
}
86-
```
78+
- **View Layer:**
79+
```javascript
80+
// In your Web Component or View Logic
81+
this.viewComponent.dispatchShadowObjectsEvent('my-custom-event', {some: 'data'});
82+
```
83+
- **Shadow World:**
84+
```typescript
85+
// Your Shadow Object
86+
export function MyLogic({onViewEvent}: ShadowObjectCreationAPI) {
87+
onViewEvent((type, data) => {
88+
if (type === 'my-custom-event') {
89+
console.log('Received from View:', data);
90+
}
91+
});
92+
}
93+
```
8794

8895
### 2. Shadow Object -> Shadow Object (Same Entity)
96+
8997
Shadow Objects attached to the same Entity can communicate via events.
9098

9199
```typescript
92-
import { emit } from '@spearwolf/eventize';
100+
import {emit} from '@spearwolf/eventize';
93101

94102
// Feature A
95-
export function FeatureA({ on }: ShadowObjectCreationAPI) {
103+
export function FeatureA({on}: ShadowObjectCreationAPI) {
96104
// Listen for event from Feature B
97-
on('data-loaded', (data) => { /* ... */ });
105+
on('data-loaded', (data) => {
106+
/* ... */
107+
});
98108
}
99109

100110
// Feature B
101-
export function FeatureB({ entity }: ShadowObjectCreationAPI) {
111+
export function FeatureB({entity}: ShadowObjectCreationAPI) {
102112
// Emit event via the entity
103-
emit(entity, 'data-loaded', { id: 123 });
113+
emit(entity, 'data-loaded', {id: 123});
104114
}
105115
```
106116

107117
### 3. Broadcasting to Children (Traverse the Entity Tree)
118+
108119
You can broadcast events to all descendant Entities using the `traverse` helper. This is useful for "global" updates like a frame tick or a resize event.
109120

110121
```typescript
111-
import { emit } from '@spearwolf/eventize';
122+
import {emit} from '@spearwolf/eventize';
112123

113-
export function StageController({ entity, on }: ShadowObjectCreationAPI) {
124+
export function StageController({entity, on}: ShadowObjectCreationAPI) {
114125
// Example: Broadcast a 'frame-update' event to the entire subtree
115126
on('tick', (deltaTime) => {
116127
entity.traverse((e) => {
117-
emit(e, 'frame-update', { deltaTime });
128+
emit(e, 'frame-update', {deltaTime});
118129
});
119130
});
120131
}
@@ -124,21 +135,26 @@ export function StageController({ entity, on }: ShadowObjectCreationAPI) {
124135
> `traverse()` visits first the current entity and then all its descendants recursively.
125136
126137
### 4. Shadow -> View Events
138+
127139
You can send events back to the View Layer using `dispatchMessageToView`.
128140

129-
* **Shadow World:**
130-
```typescript
131-
export function MyLogic({ dispatchMessageToView }: ShadowObjectCreationAPI) {
132-
dispatchMessageToView('notify', { message: 'Save successful!' });
133-
}
134-
```
135-
* **View Layer:**
136-
```javascript
137-
import {on} from '@spearwolf/eventize';
138-
139-
on(viewComponent, {
140-
notify(data) {
141-
console.log('Received a notification:', data);
142-
}
143-
});
144-
```
141+
- **Shadow World:**
142+
```typescript
143+
export function MyLogic({dispatchMessageToView}: ShadowObjectCreationAPI) {
144+
dispatchMessageToView('notify', {message: 'Save successful!'});
145+
}
146+
```
147+
- **View Layer:**
148+
149+
```javascript
150+
import {on} from '@spearwolf/eventize';
151+
152+
on(viewComponent, {
153+
notify(data) {
154+
console.log('Received a notification:', data);
155+
},
156+
});
157+
```
158+
159+
> [!TIP]
160+
> **Forwarding to DOM:** If you are using `<shae-ent>`, you can automatically forward these events to the DOM element using the `forward-custom-events` attribute. See the [Web Components API](../03-api/04-web-components.md#forward-custom-events) for details.

0 commit comments

Comments
 (0)