Skip to content

Commit 2412e90

Browse files
committed
refactor: update dispatchMessageToView usage across the codebase
1 parent 35ed63f commit 2412e90

12 files changed

Lines changed: 93 additions & 53 deletions

File tree

packages/shadow-objects-e2e/public/mod-hello.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
function foo({entity, useProperty}) {
1+
function foo({useProperty, dispatchMessageToView}) {
22
const xyz = useProperty('xyz');
33

44
console.log('ShadowObject "foo" created: xyz=', xyz());
55

66
xyz((val) => {
77
console.log('foo.xyz changed to', val);
8-
entity.dispatchMessageToView('fooEcho', xyz());
8+
dispatchMessageToView('fooEcho', xyz());
99
});
1010

11-
entity.dispatchMessageToView('helloFromFoo', {xyz: xyz()});
11+
dispatchMessageToView('helloFromFoo', {xyz: xyz()});
1212
}
1313

1414
export const shadowObjects = {

packages/shadow-objects/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- sharpen the `EntityApi` type definitions
1111

12+
## [0.27.0] - 2026-01-19
13+
14+
### ⚠️ Breaking Changes
15+
16+
- **API Update:** `dispatchMessageToView` has been moved from the `entity` instance to the `ShadowObjectCreationAPI`.
17+
- **Before:** `entity.dispatchMessageToView(...)`
18+
- **After:** `dispatchMessageToView(...)` (available as an argument in the constructor/factory function)
19+
- **Type Definitions:** Removed `dispatchMessageToView` from `EntityApi` interface.
20+
1221
## [0.26.4] - 2026-01-15
1322

1423
- fix return type definitions for `provideContext()` and `provideGlobalContext()`

packages/shadow-objects/docs/02-guides/01-getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export default {
140140
* `CounterLogic` receives `onViewEvent`.
141141
* `setCount` updates the signal.
142142
* `createEffect` runs.
143-
* **Events:** The Shadow Object uses `entity.dispatchMessageToView` to communicate back (see View Integration).
143+
* **Events:** The Shadow Object uses `dispatchMessageToView` to communicate back (see View Integration).
144144

145145
## Next Steps
146146

packages/shadow-objects/docs/02-guides/02-creating-shadow-objects.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,12 @@ createEffect(() => {
108108
To update the **View Component**, you should use events. This keeps the data flow unidirectional and explicit.
109109

110110
```typescript
111-
export function CounterLogic({ entity, createSignal, createEffect }) {
111+
export function CounterLogic({ createSignal, createEffect, dispatchMessageToView }) {
112112
const [count, setCount] = createSignal(0);
113113

114114
// Notify View of changes
115115
createEffect(() => {
116-
entity.dispatchMessageToView('count-changed', { value: count() });
116+
dispatchMessageToView('count-changed', { value: count() });
117117
});
118118
}
119119
```
@@ -139,7 +139,7 @@ on(entity, 'onViewEvent', (type, data) => {
139139
You can also send events *up* to the View.
140140

141141
```typescript
142-
entity.dispatchMessageToView('loginSuccess', { user: 'Alice' });
142+
dispatchMessageToView('loginSuccess', { user: 'Alice' });
143143
```
144144

145145
In the View Layer, you would listen for this event on the component.

packages/shadow-objects/docs/02-guides/03-view-integration.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ The Shadow Object receives this via `on(entity, 'onViewEvent', ...)`.
7676

7777
### Receiving Events from Shadow World
7878

79-
When a Shadow Object calls `entity.dispatchMessageToView('my-event', data)`, the `<shae-ent>` element dispatches a CustomEvent.
79+
When a Shadow Object calls `dispatchMessageToView('my-event', data)`, the `<shae-ent>` element dispatches a CustomEvent.
8080

8181
```javascript
8282
ent.addEventListener('my-event', (e) => {
@@ -90,7 +90,7 @@ To update the UI based on state changes in the Shadow Object, you should emit ev
9090
// Shadow Object
9191
createEffect(() => {
9292
// Whenever count changes, notify the view
93-
entity.dispatchMessageToView('count-changed', { value: count() });
93+
dispatchMessageToView('count-changed', { value: count() });
9494
});
9595
```
9696

@@ -100,7 +100,7 @@ To update the UI based on state changes in the Shadow Object, you should emit ev
100100
// Shadow Object
101101
createEffect(() => {
102102
// Whenever count changes, notify the view
103-
entity.dispatchMessageToView('count-changed', { value: count() });
103+
dispatchMessageToView('count-changed', { value: count() });
104104
});
105105

106106
// View Layer

packages/shadow-objects/docs/03-api/01-shadow-object-api.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,35 +161,50 @@ Same as `on`, but the listener is automatically removed after the first trigger.
161161

162162
---
163163

164-
## 5. Lifecycle
164+
## 5. View Integration
165165

166-
### `onDestroy(callback)`
166+
Shadow Objects can communicate directly with the View Layer (the DOM) by dispatching messages.
167167

168-
Registers a cleanup function. This is critical for preventing memory leaks when using non-framework resources (like `setInterval`).
168+
### `dispatchMessageToView(type, data?, transferables?, traverseChildren?)`
169169

170-
* **Signature:** `onDestroy(fn: () => void): void`
170+
Sends an event **from** the Shadow World **to** the View Layer. The `<shae-ent>` DOM element will dispatch a `CustomEvent`.
171+
172+
* **Signature:** `dispatchMessageToView(type: string, data?: unknown, transferables?: Transferable[], traverseChildren?: boolean): void`
173+
174+
**Parameters:**
175+
* `type`: The name of the custom event to dispatch on the `<shae-ent>` element.
176+
* `data`: (Optional) Data to send as `event.detail`.
177+
* `transferables`: (Optional) Array of transferable objects (like `ArrayBuffer`, `MessagePort`) to transfer ownership of, instead of cloning.
178+
* `traverseChildren`: (Optional) If `true`, the event will be dispatched to the corresponding view component and all its descendants in the view hierarchy. Defaults to `false`.
171179

172180
```typescript
173-
const interval = setInterval(tick, 1000);
174-
onDestroy(() => clearInterval(interval));
181+
// Shadow World
182+
dispatchMessageToView('login-success', { user: 'Alice' });
183+
184+
// View Layer (DOM)
185+
el.addEventListener('login-success', (e) => console.log(e.detail.user));
175186
```
176187

177188
---
178189

179-
## 6. The `entity` Instance
190+
## 6. Lifecycle
180191

181-
The API provides direct access to the underlying `EntityApi` instance via the `entity` property.
182192

183-
### `entity.dispatchMessageToView(type, detail)`
193+
### `onDestroy(callback)`
184194

185-
Sends an event **from** the Shadow World **to** the View Layer. The `<shae-ent>` DOM element will dispatch a `CustomEvent`.
195+
Registers a cleanup function. This is critical for preventing memory leaks when using non-framework resources (like `setInterval`).
186196

187-
* **Signature:** `entity.dispatchMessageToView(type: string, detail?: any): void`
197+
* **Signature:** `onDestroy(fn: () => void): void`
188198

189199
```typescript
190-
// Shadow World
191-
entity.dispatchMessageToView('login-success', { user: 'Alice' });
192-
193-
// View Layer (DOM)
194-
el.addEventListener('login-success', (e) => console.log(e.detail.user));
200+
const interval = setInterval(tick, 1000);
201+
onDestroy(() => clearInterval(interval));
195202
```
203+
204+
---
205+
206+
## 7. The `entity` Instance
207+
208+
The API provides direct access to the underlying `EntityApi` instance via the `entity` property. This gives access to entity metadata like `uuid`, `order`, hierarchy info (`parent`, `children`), and property inspection (`propKeys`, `propEntries`).
209+
210+
Note: `dispatchMessageToView` is now a top-level method on the API object and is no longer available on the `entity` instance.

packages/shadow-objects/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@spearwolf/shadow-objects",
33
"description": "a reactive entity-component framework that feels at home in the shadows",
4-
"version": "0.26.4",
4+
"version": "0.27.0",
55
"author": {
66
"name": "Wolfger Schramm",
77
"email": "wolfger@spearwolf.de",

packages/shadow-objects/src/in-the-dark/Kernel.spec.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,29 @@ describe('Kernel', () => {
166166
});
167167

168168
describe('MessageToView with traverseChildren', () => {
169+
const registry = new Registry();
170+
171+
// Helper class to expose dispatchMessageToView for testing
172+
@ShadowObject({registry, token: 'test'})
173+
class TestDispatcher {
174+
dispatchMessageToView: ShadowObjectCreationAPI['dispatchMessageToView'];
175+
constructor({dispatchMessageToView}: ShadowObjectCreationAPI) {
176+
this.dispatchMessageToView = dispatchMessageToView;
177+
}
178+
}
179+
expect(TestDispatcher).toBeDefined();
180+
169181
it('should emit MessageToView event with traverseChildren=false by default', async () => {
170-
const kernel = new Kernel();
182+
const kernel = new Kernel(registry);
171183
const uuid = generateUUID();
172184

173185
kernel.createEntity(uuid, 'test');
174-
const entity = kernel.getEntity(uuid);
186+
const so = kernel.findShadowObjects(uuid)[0] as any;
175187

176188
const messageToViewSpy = vi.fn();
177189
on(kernel, MessageToView, messageToViewSpy);
178190

179-
entity.dispatchMessageToView('testType', {payload: 'data'});
191+
so.dispatchMessageToView('testType', {payload: 'data'});
180192

181193
// Wait for queueMicrotask to complete
182194
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
@@ -193,16 +205,16 @@ describe('Kernel', () => {
193205
});
194206

195207
it('should emit MessageToView event with traverseChildren=true when specified', async () => {
196-
const kernel = new Kernel();
208+
const kernel = new Kernel(registry);
197209
const uuid = generateUUID();
198210

199211
kernel.createEntity(uuid, 'test');
200-
const entity = kernel.getEntity(uuid);
212+
const so = kernel.findShadowObjects(uuid)[0] as any;
201213

202214
const messageToViewSpy = vi.fn();
203215
on(kernel, MessageToView, messageToViewSpy);
204216

205-
entity.dispatchMessageToView('broadcastEvent', {message: 'hello'}, undefined, true);
217+
so.dispatchMessageToView('broadcastEvent', {message: 'hello'}, undefined, true);
206218

207219
// Wait for queueMicrotask to complete
208220
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));
@@ -219,17 +231,17 @@ describe('Kernel', () => {
219231
});
220232

221233
it('should emit MessageToView with transferables', async () => {
222-
const kernel = new Kernel();
234+
const kernel = new Kernel(registry);
223235
const uuid = generateUUID();
224236

225237
kernel.createEntity(uuid, 'test');
226-
const entity = kernel.getEntity(uuid);
238+
const so = kernel.findShadowObjects(uuid)[0] as any;
227239

228240
const messageToViewSpy = vi.fn();
229241
on(kernel, MessageToView, messageToViewSpy);
230242

231243
const buffer = new ArrayBuffer(8);
232-
entity.dispatchMessageToView('dataEvent', {buffer}, [buffer], false);
244+
so.dispatchMessageToView('dataEvent', {buffer}, [buffer], false);
233245

234246
// Wait for queueMicrotask to complete
235247
await new Promise((resolve) => queueMicrotask(() => resolve(undefined)));

packages/shadow-objects/src/in-the-dark/Kernel.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,10 @@ export class Kernel {
517517
return ctxReader;
518518
},
519519

520+
dispatchMessageToView(type: string, data?: unknown, transferables?: Transferable[], traverseChildren = false) {
521+
entry.entity.dispatchMessageToView(type, data, transferables, traverseChildren);
522+
},
523+
520524
useProperty: getUseProperty,
521525

522526
useProperties<K extends string>(props: Record<K, string>): Record<K, SignalReader<any>> {

packages/shadow-objects/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface AppliedChangeTrailEvent {
8686
}
8787

8888
export type EntityApi = Readonly<
89-
Pick<Entity, 'uuid' | 'order' | 'hasParent' | 'dispatchMessageToView' | 'propKeys' | 'propEntries'> & {
89+
Pick<Entity, 'uuid' | 'order' | 'hasParent' | 'propKeys' | 'propEntries'> & {
9090
parent?: EntityApi;
9191
children: readonly EntityApi[];
9292
traverse(callback: (entity: EntityApi) => unknown): void;
@@ -106,6 +106,8 @@ export type Maybe<T = unknown> = NonNullable<T> | undefined;
106106
export interface ShadowObjectCreationAPI {
107107
entity: EntityApi;
108108

109+
dispatchMessageToView(type: string, data?: unknown, transferables?: TransferablesType, traverseChildren?: boolean): void;
110+
109111
provideContext<T = unknown>(
110112
name: string | symbol,
111113
sourceOrInitialValue?: T | SignalReader<T | undefined>,

0 commit comments

Comments
 (0)