Skip to content

Commit a643e6e

Browse files
authored
Merge pull request #75 from koculu/context-registry
Add context registry
2 parents ffb00f3 + 7b05c1d commit a643e6e

File tree

10 files changed

+487
-4
lines changed

10 files changed

+487
-4
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
title: contextRegistry
3+
---
4+
5+
## Overview
6+
7+
`ContextRegistry` is a utility class for explicit context sharing across
8+
component trees.
9+
10+
It stores context instances by constructor and lets you resolve them by type.
11+
Lookup uses `instanceof`, so querying by a base class can return a subclass
12+
instance.
13+
14+
## Usage
15+
16+
```ts
17+
import { ContextRegistry } from 'regor'
18+
19+
class AppServices {
20+
readonly apiBase = '/v1'
21+
}
22+
23+
const registry = new ContextRegistry()
24+
registry.register(new AppServices())
25+
26+
const services = registry.require(AppServices)
27+
```
28+
29+
## API
30+
31+
### `register(context)`
32+
33+
Registers an instance under its runtime constructor.
34+
35+
- If another instance of the same constructor is already registered, it is
36+
replaced.
37+
38+
### `unregisterByClass(ContextClass)`
39+
40+
Removes the entry for a constructor.
41+
42+
- No-op when that constructor is not registered.
43+
44+
### `unregister(context)`
45+
46+
Removes an instance only if it is currently the registered value for its
47+
constructor.
48+
49+
- This prevents deleting a newer replacement instance registered later.
50+
51+
### `find(ContextClass)`
52+
53+
Returns the first registered instance matching `instanceof ContextClass`, or
54+
`undefined`.
55+
56+
### `require(ContextClass)`
57+
58+
Returns the same result as `find`, but throws when not found.
59+
60+
- Error message format: `ClassName is not registered in ContextRegistry.`
61+
62+
## Example: parent provides registry, child resolves from `head.requireContext`
63+
64+
```ts
65+
import { ComponentHead, ContextRegistry, defineComponent } from 'regor'
66+
67+
class AppServices {
68+
readonly apiBase = '/v1'
69+
}
70+
71+
class Parent {
72+
readonly registry = new ContextRegistry()
73+
74+
constructor() {
75+
this.registry.register(new AppServices())
76+
}
77+
}
78+
79+
class Child {
80+
readonly apiBase: string
81+
82+
constructor(head: ComponentHead<object>) {
83+
// Parent can provide registry from any component context shape.
84+
const parent = head.requireContext(Parent)
85+
const services = parent.registry.require(AppServices)
86+
this.apiBase = services.apiBase
87+
}
88+
}
89+
90+
const parent = defineComponent<Parent>('<div><slot></slot></div>', {
91+
context: () => new Parent(),
92+
})
93+
94+
const child = defineComponent<Child>('<p r-text="apiBase"></p>', {
95+
context: (head) => {
96+
return new Child(head)
97+
},
98+
})
99+
```
100+
101+
## Example: component ctx <=> component ctx communication through parent registry
102+
103+
```ts
104+
import {
105+
ComponentHead,
106+
ContextRegistry,
107+
createApp,
108+
defineComponent,
109+
html,
110+
ref,
111+
} from 'regor'
112+
113+
class Parent {
114+
components = { producer, consumer }
115+
registry = new ContextRegistry()
116+
}
117+
118+
class Consumer {
119+
message = ref('idle')
120+
private readonly parent: Parent
121+
122+
constructor(head: ComponentHead<object>) {
123+
this.parent = head.requireContext(Parent)
124+
this.parent.registry.register(this)
125+
}
126+
127+
receive = (next: string): void => {
128+
this.message(next)
129+
}
130+
}
131+
132+
class Producer {
133+
private readonly parent: Parent
134+
135+
constructor(head: ComponentHead<object>) {
136+
this.parent = head.requireContext(Parent)
137+
this.parent.registry.register(this)
138+
}
139+
140+
send = (): void => {
141+
// direct component-ctx -> component-ctx call through registry
142+
const consumer = this.parent.registry.require(Consumer)
143+
consumer.receive('message-from-producer')
144+
}
145+
}
146+
147+
const producer = defineComponent<Producer>(
148+
html`<button class="send" @click="send">send</button>`,
149+
{
150+
context: (head) => new Producer(head),
151+
},
152+
)
153+
154+
const consumer = defineComponent<Consumer>(
155+
html`<p class="value">{{ message }}</p>`,
156+
{
157+
context: (head) => new Consumer(head),
158+
},
159+
)
160+
161+
const parent = defineComponent<Parent>(
162+
html`<section>
163+
<Producer></Producer>
164+
<Consumer></Consumer>
165+
</section>`,
166+
{
167+
context: () => new Parent(),
168+
},
169+
)
170+
171+
createApp(
172+
{ components: { parent } },
173+
{
174+
element: document.querySelector('#app')!,
175+
template: '<Parent></Parent>',
176+
},
177+
)
178+
```
179+
180+
## See Also
181+
182+
- [`defineComponent`](/api/defineComponent)
183+
- [`useScope`](/api/useScope)
184+
- [Components Guide](/guide/components)
185+
- [TypeScript Guide](/guide/typescript)
186+
187+
[Back to the API list](/api/)

docs-site/src/content/docs/api/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Use this page as an index, then open each API page for signature details and exa
5151
1. [`useScope`](/api/useScope)
5252
2. [`onMounted`](/api/onMounted)
5353
3. [`onUnmounted`](/api/onUnmounted)
54+
4. [`contextRegistry`](/api/contextRegistry)
5455

5556
## Cleanup and Unbind
5657

docs-site/src/content/docs/guide/components.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,4 @@ Unmount cleans child component bindings and observers.
231231
3. [Directive: :context](/directives/context)
232232
4. [TypeScript Guide](/guide/typescript)
233233
5. [Directive: r-for](/directives/r-for)
234+
6. [contextRegistry API](/api/contextRegistry)

docs-site/src/content/docs/guide/typescript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,4 @@ class ChildContext {
127127
1. [Components](/guide/components)
128128
2. [defineComponent API](/api/defineComponent)
129129
3. [createApp API](/api/createApp)
130+
4. [contextRegistry API](/api/contextRegistry)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "regor",
3-
"version": "1.3.8",
3+
"version": "1.3.9",
44
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
55
"author": "Ahmed Yasin Koculu",
66
"license": "MIT",

src/app/ComponentHead.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { type IRegorContext } from '../api/types'
22
import { removeNode } from '../cleanup/removeNode'
33
import { callUnmounted } from '../composition/callUnmounted'
44

5-
export type ContextClass<TValue> = abstract new (...args: never[]) => TValue
5+
export type ContextClass<TValue extends object> = abstract new (
6+
...args: never[]
7+
) => TValue
68

79
/**
810
* Runtime metadata passed to a component's `context(head)` factory.
@@ -185,7 +187,7 @@ export class ComponentHead<
185187
* @param occurrence - Zero-based index of the matching instance to return.
186188
* @returns The matching parent context instance, or `undefined` when not found.
187189
*/
188-
findContext<TValue>(
190+
findContext<TValue extends object>(
189191
constructor: ContextClass<TValue>,
190192
occurrence = 0,
191193
): TValue | undefined {
@@ -218,7 +220,7 @@ export class ComponentHead<
218220
* @returns The parent context instance at the requested occurrence.
219221
* @throws Error when no matching instance exists at the requested occurrence.
220222
*/
221-
requireContext<TValue>(
223+
requireContext<TValue extends object>(
222224
constructor: ContextClass<TValue>,
223225
occurrence = 0,
224226
): TValue {

src/composition/ContextRegistry.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { type ContextClass } from '../app/ComponentHead'
2+
3+
/**
4+
* Registry for sharing typed context instances across component boundaries.
5+
*
6+
* Entries are keyed by the runtime constructor of each registered instance:
7+
* - one active entry per concrete constructor
8+
* - registering another instance of the same constructor replaces the previous
9+
*
10+
* Lookup is type-based and uses `instanceof`, so querying a base class can
11+
* resolve a registered subclass instance.
12+
*/
13+
export class ContextRegistry {
14+
private readonly byConstructor = new Map<ContextClass<object>, object>()
15+
16+
/**
17+
* Registers a context instance under its concrete runtime constructor.
18+
*
19+
* If an instance with the same constructor already exists, it is replaced.
20+
*
21+
* @param context - Context instance to register.
22+
*/
23+
register<TContext extends object>(context: TContext): void {
24+
this.byConstructor.set(context.constructor as ContextClass<object>, context)
25+
}
26+
27+
/**
28+
* Removes the entry for a constructor.
29+
*
30+
* No-op when the constructor is not registered.
31+
*
32+
* @param contextClass - Constructor key to remove.
33+
*/
34+
unregisterByClass<TContext extends object>(
35+
contextClass: ContextClass<TContext>,
36+
): void {
37+
this.byConstructor.delete(contextClass)
38+
}
39+
40+
/**
41+
* Removes a specific context instance if it is currently registered for its
42+
* constructor.
43+
*
44+
* This prevents deleting a newer replacement instance of the same class.
45+
*
46+
* @param context - Context instance to remove.
47+
*/
48+
unregister<TContext extends object>(context: TContext): void {
49+
const key = context.constructor as ContextClass<object>
50+
if (this.byConstructor.get(key) === context) {
51+
this.byConstructor.delete(key)
52+
}
53+
}
54+
55+
/**
56+
* Finds a context instance by constructor type.
57+
*
58+
* The registry is scanned in insertion order and each entry is checked with
59+
* `instanceof contextClass`.
60+
*
61+
* @param contextClass - Class to match via `instanceof`.
62+
* @returns Matching instance, or `undefined` when not found.
63+
*/
64+
find<TContext extends object>(
65+
contextClass: ContextClass<TContext>,
66+
): TContext | undefined {
67+
for (const value of this.byConstructor.values()) {
68+
if (value instanceof contextClass) return value
69+
}
70+
return undefined
71+
}
72+
73+
/**
74+
* Resolves a context instance by constructor type and guarantees a value.
75+
*
76+
* @param contextClass - Class to match via `instanceof`.
77+
* @returns Matching context instance.
78+
* @throws Error when no matching context is registered.
79+
*/
80+
require<TContext extends object>(
81+
contextClass: ContextClass<TContext>,
82+
): TContext {
83+
const value = this.find(contextClass)
84+
if (value) return value
85+
throw new Error(
86+
`${contextClass.name} is not registered in ContextRegistry.`,
87+
)
88+
}
89+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export { getBindData } from './cleanup/getBindData'
3333
export { drainUnbind } from './cleanup/removeNode'
3434
export { removeNode } from './cleanup/removeNode'
3535
export { unbind } from './cleanup/unbind'
36+
export { ContextRegistry } from './composition/ContextRegistry'
3637
export { onMounted } from './composition/onMounted'
3738
export { onUnmounted } from './composition/onUnmounted'
3839
export { useScope } from './composition/useScope'

0 commit comments

Comments
 (0)