|
1 | 1 | _hi! welcome to .._ |
2 | | -# shadow-objects 🧛 |
3 | | - |
4 | | -## Introduction |
5 | | - |
6 | | -_Shadow-objects_ is a standalone, reactive entity←component framework 🔥 |
7 | | - |
8 | | -The original idea is visualized in this overview: |
9 | | - |
10 | | - |
11 | | - |
12 | | -> _ents_ is short for latin _"entitatis"_, which translates to _"shadow entities"_ |
13 | | -
|
14 | | -> [!IMPORTANT] |
15 | | -> _Dear adventurer, be warned: everything here is highly experimental and in active development and constant flux!_ |
16 | | -
|
17 | | -The components are created in the view space. Hierarchical. In your browser. There is a JavaScript API for this. But to keep things simple, there are also ready-to-use web components. |
18 | | - |
19 | | -```html |
20 | | -<shae-worker-env local? src="./my-personal-shadow-objects.js" /> |
21 | | - |
22 | | -<shae-ent ns? token="foo"> |
23 | | - <shae-ent ns? token="bar"> |
24 | | - <shae-prop name="myNumbers" value="1 2 3" type="integer[]" /> |
25 | | - </shae-ent> |
26 | | -</shae-ent> |
27 | | -``` |
28 | | - |
29 | | -In the shadows, the _shadow objects_ come and go with the entities. An entity can cover many shadows. But all are united by the entity _token_. That token, which is specified by the _view components_. |
30 | | - |
31 | | -The _shadow object module_ stores which shadow objects are hidden behind a token. Either a _function_ or a _class_. |
32 | | - |
33 | | -```js |
34 | | -shadowObjects.define('token', function() { |
35 | | - console.log('The simplest shadow object ever.'); |
36 | | -}); |
37 | | - |
38 | | -@ShadowObject('token') |
39 | | -class Foo { |
40 | | - constructor({ |
41 | | - entity, |
42 | | - provideContext, |
43 | | - useContext, |
44 | | - useProperty, |
45 | | - onDestroy, |
46 | | - }: ShadowObjectParams) { |
47 | | - console.log('hello'); |
48 | | - |
49 | | - onDestroy(() => { |
50 | | - console.log('bye'); |
51 | | - }); |
52 | | - } |
| 2 | +# Shadow Objects Framework 🧛 |
| 3 | + |
| 4 | +The **Shadow Objects Framework** is a library for managing application state and logic that runs "in the dark" (in a worker or separate context), mirroring a view hierarchy (like the DOM). It decouples business logic and state management from the UI rendering layer. |
| 5 | + |
| 6 | +## Core Concepts |
| 7 | + |
| 8 | +### 1. Entities |
| 9 | +An **Entity** is the fundamental unit in the framework. It represents a node in the hierarchy, mirroring a view component (e.g., a Web Component or a DOM element). |
| 10 | +- **Hierarchy**: Entities have parents and children, forming a tree structure. |
| 11 | +- **Properties**: Entities hold reactive properties that sync with the view. |
| 12 | +- **Context**: Entities participate in a hierarchical context system (dependency injection). |
| 13 | + |
| 14 | +### 2. Shadow Objects |
| 15 | +A **Shadow Object** is a functional unit of logic attached to an Entity. |
| 16 | +- **Logic Containers**: They contain the state, effects, and business logic for a specific feature. |
| 17 | +- **Lifecycle**: They are automatically created and destroyed by the **Kernel** based on the Entity's **Token**. |
| 18 | +- **Reactivity**: They use **Signals** and **Effects** (via `@spearwolf/signalize`) to react to changes in properties or context. |
| 19 | + |
| 20 | +### 3. The Kernel |
| 21 | +The **Kernel** is the brain of the framework. |
| 22 | +- **Manages Entities**: Handles creation, destruction, and hierarchy updates of Entities. |
| 23 | +- **Orchestrates Shadow Objects**: Instantiates the correct Shadow Objects for each Entity based on its Token and the Registry. |
| 24 | +- **Message Dispatch**: Handles communication between the View (UI) and the Shadow World. |
| 25 | + |
| 26 | +### 4. The Registry |
| 27 | +The **Registry** maps **Tokens** to **Shadow Object Constructors**. |
| 28 | +- **Tokens**: Strings that identify what logic an Entity should have (e.g., `"my-component"`). |
| 29 | +- **Routes**: Defines rules for composing multiple Shadow Objects. For example, a token can "route" to other tokens, causing multiple Shadow Objects to be instantiated for a single Entity. |
| 30 | +- **Conditional Routing**: Routes can be triggered based on the presence of specific "truthy" properties on the Entity (e.g., `@myProp` routes only if `myProp` is set). |
| 31 | + |
| 32 | +## Lifecycle |
| 33 | + |
| 34 | +1. **Creation**: When an Entity is created (e.g., a Web Component connects), the Kernel looks up its Token in the Registry. |
| 35 | +2. **Instantiation**: The Kernel instantiates all Shadow Objects associated with that Token (and its routes). |
| 36 | +3. **Execution**: The Shadow Object function runs, setting up signals, effects, and context providers. |
| 37 | +4. **Updates**: |
| 38 | + * **Properties**: When view properties change, the Entity's signals update, triggering any dependent effects in the Shadow Object. |
| 39 | + * **Context**: If a parent Entity changes a provided context, child Shadow Objects consuming that context automatically update. |
| 40 | +5. **Destruction**: When an Entity is removed or its Token changes, the Kernel destroys the associated Shadow Objects, cleaning up all signals and effects. |
| 41 | + |
| 42 | +## API: Writing Shadow Objects |
| 43 | + |
| 44 | +Shadow Objects are defined as functions (or classes) that receive a `ShadowObjectParams` object. |
| 45 | + |
| 46 | +```typescript |
| 47 | +import { ShadowObjectParams } from "@spearwolf/shadow-objects"; |
| 48 | + |
| 49 | +export function MyShadowObject({ |
| 50 | + useProperty, |
| 51 | + useProperties, |
| 52 | + useContext, |
| 53 | + provideContext, |
| 54 | + useResource, |
| 55 | + createEffect, |
| 56 | + on |
| 57 | +}: ShadowObjectParams) { |
| 58 | + |
| 59 | + // 1. Read Properties from the View |
| 60 | + const getTitle = useProperty("title"); |
| 61 | + // or bulk access: |
| 62 | + const props = useProperties({ |
| 63 | + title: "title", |
| 64 | + isEnabled: "enabled" |
| 65 | + }); |
| 66 | + |
| 67 | + // 2. Consume Context from Parents |
| 68 | + const getTheme = useContext("theme"); |
| 69 | + |
| 70 | + // 3. Manage Resources (Lifecycle) |
| 71 | + // Automatically creates/destroys the resource when dependencies change |
| 72 | + const myService$ = useResource(() => { |
| 73 | + const title = props.title(); |
| 74 | + if (!title) return; |
| 75 | + return new MyService(title); |
| 76 | + }, (service) => service.dispose()); |
| 77 | + |
| 78 | + // 4. Provide Context to Children |
| 79 | + // Can pass a value or a signal directly |
| 80 | + provideContext("myService", myService$); |
| 81 | + |
| 82 | + // 5. Side Effects |
| 83 | + createEffect(() => { |
| 84 | + console.log("Title changed to:", props.title()); |
| 85 | + }); |
| 86 | + |
| 87 | + // 6. Event Listeners |
| 88 | + on(entity, "some-event", (data) => { /* ... */ }); |
53 | 89 | } |
54 | 90 | ``` |
55 | 91 |
|
56 | | -> [!WARNING] |
57 | | -> Sorry, at this point there should be a precise and crisp introduction to the concepts of the framework, but unfortunately this is not currently available. |
58 | | -> Instead of that, a few insights into the implementation will follow |
| 92 | +### Key API Methods |
59 | 93 |
|
60 | | -## 📖 Shadow Objects CHEAT SHEET |
| 94 | +* **`useProperty(name)`**: Returns a signal reader for a specific property on the Entity. |
| 95 | +* **`useProperties(map)`**: Returns an object of signal readers for multiple properties. |
| 96 | +* **`useContext(name)`**: Returns a signal reader for a value provided by a parent Entity. |
| 97 | +* **`provideContext(name, value)`**: Makes a value (or signal) available to descendant Entities. |
| 98 | +* **`useResource(factory, cleanup)`**: A helper to create and manage objects that need explicit cleanup (like Three.js objects or subscriptions). |
| 99 | +* **`createEffect(callback)`**: Runs a side effect whenever accessed signals change. |
| 100 | +* **`createSignal(initialValue)`**: Creates a local reactive state. |
61 | 101 |
|
62 | | -There are two ways to create a _shadow object component_: either as a _function_ or as a _class_: |
| 102 | +## Architecture Diagram |
63 | 103 |
|
64 | | -### Create Shadow Object by FUNCTION |
| 104 | +```mermaid |
| 105 | +graph TD |
| 106 | + View[View / DOM] -->|Messages| Kernel |
| 107 | + Kernel -->|Updates| EntityTree[Entity Tree] |
65 | 108 |
|
66 | | -```js |
67 | | -import { onDestroy, type ShadowObjectParams, type Entity } from "@spearwolf/shadow-objects/shadow-objects.js"; |
| 109 | + subgraph "Shadow World" |
| 110 | + EntityTree |
| 111 | + Entity[Entity] |
| 112 | + SO[Shadow Object] |
68 | 113 |
|
69 | | -function MyShadowObject(params: ShadowObjectParams) { |
70 | | - // |
71 | | - // ... PUT YOUR IMPLEMENTATION HERE ... |
72 | | - // |
73 | | - |
74 | | - // Return an object. This step is optional. |
75 | | - return { |
76 | | - // All methods here are reactive and correspond to entity events of the same name! |
| 114 | + EntityTree --> Entity |
| 115 | + Entity -->|Has| SO |
77 | 116 |
|
78 | | - [onDestroy](entity: Entity) { |
79 | | - // Called when the shadow object is destroyed. |
80 | | - // This is one of the predefined events that a shadow object can receive. |
| 117 | + SO -->|Reads| Props[Properties] |
| 118 | + SO -->|Reads/Writes| Context |
| 119 | + SO -->|Runs| Logic[Business Logic] |
| 120 | + end |
81 | 121 |
|
82 | | - // This can happen when the entity is destroyed or the shadow object component |
83 | | - // is removed from the entity (e.g., by changing the entity token, view properties, and/or routing) |
84 | | - }, |
85 | | - |
86 | | - fooBar(plah) { |
87 | | - /* is called when the entity receives a 'fooBar' event */ |
88 | | - }, |
89 | | - }; |
90 | | -} |
| 122 | + Logic -->|Updates| Signals |
| 123 | + Signals -->|Triggers| Effects |
91 | 124 | ``` |
92 | | - |
93 | | -### Create Shadow Object by CLASS |
94 | | - |
95 | | -Essentially the same as above, but as a `class`: |
96 | | - |
97 | | -```js |
98 | | -import { onDestroy, type ShadowObjectParams, type Entity } from "@spearwolf/shadow-objects/shadow-objects.js"; |
99 | | - |
100 | | -class MyShadowObject { |
101 | | - constructor(params: ShadowObjectParams) { |
102 | | - // |
103 | | - // ... INITIALIZE SHADOW OBJECT ... |
104 | | - // |
105 | | - } |
106 | | - |
107 | | - [onDestroy](entity: Entity) { |
108 | | - // ... |
109 | | - } |
110 | | - |
111 | | - fooBar(plah) { |
112 | | - // ... |
113 | | - } |
114 | | -} |
115 | | -``` |
116 | | - |
117 | | -### Shadow Object Construction API |
118 | | - |
119 | | -The parameters that a shadow object receives when it is created contain all the important API methods for exchanging data and events with the _view_ and also with the _shadow entity hierarchy and context_. |
120 | | - |
121 | | -[The interface `ShadowObjectsParams` is defined here](./src/types.ts) |
122 | | - |
123 | | -#### Properties |
124 | | - |
125 | | -__entity__: The shadow entity. |
126 | | - |
127 | | -#### Methods |
128 | | - |
129 | | -| Name | Call Signature | Description | |
130 | | -|------|----------------|-------------| |
131 | | -| __useProperty__ | `useProperty(name, isEqual?): SignalReader` | Read access to the property value from the view | |
132 | | -| __useContext__ | `useContext(name, isEqual?): SignalReader` | Get the value for a named context. The context is derived from the shadow entity hierarchy and the shadow object's position within it. | |
133 | | -| __useParentContext__ | `useParentContext(name, isEqual?): SignalReader` | Unlike the `useContext` method, this method skips the context of the _current_ shadow entity and directly requests the context of the parent entity. This is useful when you want to provide a custom context that depends on the parent context. | |
134 | | -| __provideContext__ | `provideContext(name, initialValue?, isEqual?): Signal` | Specify a context. This context overrides (if set) the context of the same name from the entity's parent hierarchy. The context applies to the current entity and all child entities. | |
135 | | -| __provideGlobalContext__ | `provideGlobalContext(name, initialValue?, isEqual?): Signal` | Define a context that applies to _all_ entities, regardless of hierarchy. | |
136 | | -| __createEffect__ | `createEffect(...): Effect` | see [@spearwolf/signalize#createEffect()](https://github.com/spearwolf/signalize) | |
137 | | -| __createSignal__ | `createSignal(...): Signal` | see [@spearwolf/signalize#createSignal()](https://github.com/spearwolf/signalize) | |
138 | | -| __createMemo__ | `createMemo(...): SignalReader` | see [@spearwolf/signalize#createMemo()](https://github.com/spearwolf/signalize) | |
139 | | -| __on__ | `on(...): UnsubscribeCallback` | see [@spearwolf/eventize#on()](https://github.com/spearwolf/eventize) | |
140 | | -| __once__ | `once(...): UnsubscribeCallback` | see [@spearwolf/eventize#once()](https://github.com/spearwolf/eventize) | |
141 | | -| __onDestroy__ | `onDestroy(callback)` | Called when the shadow object is destroyed. | |
142 | | - |
143 | | - |
144 | | -## Documentation |
145 | | - |
146 | | -TODO ... add documentation here ... ! |
147 | | - |
148 | | -Here is the big class graph overview: |
149 | | - |
150 | | - |
151 | | -More in-depth docs here: |
152 | | -- [ShadowEnv](https://github.com/spearwolf/shadow-objects/blob/main/packages/shadow-objects/src/view/README.md) |
153 | | -- [ComponentContext](https://github.com/spearwolf/shadow-objects/blob/main/packages/shadow-objects/src/view/ComponentContext.md) |
154 | | -- [ViewComponent](https://github.com/spearwolf/shadow-objects/blob/main/packages/shadow-objects/src/view/ViewComponent.md) |
0 commit comments