Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions docs-site/src/content/docs/api/contextRegistry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
title: contextRegistry
---

## Overview

`ContextRegistry` is a utility class for explicit context sharing across
component trees.

It stores context instances by constructor and lets you resolve them by type.
Lookup uses `instanceof`, so querying by a base class can return a subclass
instance.

## Usage

```ts
import { ContextRegistry } from 'regor'

class AppServices {
readonly apiBase = '/v1'
}

const registry = new ContextRegistry()
registry.register(new AppServices())

const services = registry.require(AppServices)
```

## API

### `register(context)`

Registers an instance under its runtime constructor.

- If another instance of the same constructor is already registered, it is
replaced.

### `unregisterByClass(ContextClass)`

Removes the entry for a constructor.

- No-op when that constructor is not registered.

### `unregister(context)`

Removes an instance only if it is currently the registered value for its
constructor.

- This prevents deleting a newer replacement instance registered later.

### `find(ContextClass)`

Returns the first registered instance matching `instanceof ContextClass`, or
`undefined`.

### `require(ContextClass)`

Returns the same result as `find`, but throws when not found.

- Error message format: `ClassName is not registered in ContextRegistry.`

## Example: parent provides registry, child resolves from `head.requireContext`

```ts
import { ComponentHead, ContextRegistry, defineComponent } from 'regor'

class AppServices {
readonly apiBase = '/v1'
}

class Parent {
readonly registry = new ContextRegistry()

constructor() {
this.registry.register(new AppServices())
}
}

class Child {
readonly apiBase: string

constructor(head: ComponentHead<object>) {
// Parent can provide registry from any component context shape.
const parent = head.requireContext(Parent)
const services = parent.registry.require(AppServices)
this.apiBase = services.apiBase
}
}

const parent = defineComponent<Parent>('<div><slot></slot></div>', {
context: () => new Parent(),
})

const child = defineComponent<Child>('<p r-text="apiBase"></p>', {
context: (head) => {
return new Child(head)
},
})
```

## Example: component ctx <=> component ctx communication through parent registry

```ts
import {
ComponentHead,
ContextRegistry,
createApp,
defineComponent,
html,
ref,
} from 'regor'

class Parent {
components = { producer, consumer }
registry = new ContextRegistry()
}

class Consumer {
message = ref('idle')
private readonly parent: Parent

constructor(head: ComponentHead<object>) {
this.parent = head.requireContext(Parent)
this.parent.registry.register(this)
}

receive = (next: string): void => {
this.message(next)
}
}

class Producer {
private readonly parent: Parent

constructor(head: ComponentHead<object>) {
this.parent = head.requireContext(Parent)
this.parent.registry.register(this)
}

send = (): void => {
// direct component-ctx -> component-ctx call through registry
const consumer = this.parent.registry.require(Consumer)
consumer.receive('message-from-producer')
}
}

const producer = defineComponent<Producer>(
html`<button class="send" @click="send">send</button>`,
{
context: (head) => new Producer(head),
},
)

const consumer = defineComponent<Consumer>(
html`<p class="value">{{ message }}</p>`,
{
context: (head) => new Consumer(head),
},
)

const parent = defineComponent<Parent>(
html`<section>
<Producer></Producer>
<Consumer></Consumer>
</section>`,
{
context: () => new Parent(),
},
)

createApp(
{ components: { parent } },
{
element: document.querySelector('#app')!,
template: '<Parent></Parent>',
},
)
```

## See Also

- [`defineComponent`](/api/defineComponent)
- [`useScope`](/api/useScope)
- [Components Guide](/guide/components)
- [TypeScript Guide](/guide/typescript)

[Back to the API list](/api/)
1 change: 1 addition & 0 deletions docs-site/src/content/docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Use this page as an index, then open each API page for signature details and exa
1. [`useScope`](/api/useScope)
2. [`onMounted`](/api/onMounted)
3. [`onUnmounted`](/api/onUnmounted)
4. [`contextRegistry`](/api/contextRegistry)

## Cleanup and Unbind

Expand Down
1 change: 1 addition & 0 deletions docs-site/src/content/docs/guide/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,4 @@ Unmount cleans child component bindings and observers.
3. [Directive: :context](/directives/context)
4. [TypeScript Guide](/guide/typescript)
5. [Directive: r-for](/directives/r-for)
6. [contextRegistry API](/api/contextRegistry)
1 change: 1 addition & 0 deletions docs-site/src/content/docs/guide/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ class ChildContext {
1. [Components](/guide/components)
2. [defineComponent API](/api/defineComponent)
3. [createApp API](/api/createApp)
4. [contextRegistry API](/api/contextRegistry)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regor",
"version": "1.3.8",
"version": "1.3.9",
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
"author": "Ahmed Yasin Koculu",
"license": "MIT",
Expand Down
8 changes: 5 additions & 3 deletions src/app/ComponentHead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { type IRegorContext } from '../api/types'
import { removeNode } from '../cleanup/removeNode'
import { callUnmounted } from '../composition/callUnmounted'

export type ContextClass<TValue> = abstract new (...args: never[]) => TValue
export type ContextClass<TValue extends object> = abstract new (
...args: never[]
) => TValue

/**
* Runtime metadata passed to a component's `context(head)` factory.
Expand Down Expand Up @@ -185,7 +187,7 @@ export class ComponentHead<
* @param occurrence - Zero-based index of the matching instance to return.
* @returns The matching parent context instance, or `undefined` when not found.
*/
findContext<TValue>(
findContext<TValue extends object>(
constructor: ContextClass<TValue>,
occurrence = 0,
): TValue | undefined {
Expand Down Expand Up @@ -218,7 +220,7 @@ export class ComponentHead<
* @returns The parent context instance at the requested occurrence.
* @throws Error when no matching instance exists at the requested occurrence.
*/
requireContext<TValue>(
requireContext<TValue extends object>(
constructor: ContextClass<TValue>,
occurrence = 0,
): TValue {
Expand Down
89 changes: 89 additions & 0 deletions src/composition/ContextRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { type ContextClass } from '../app/ComponentHead'

/**
* Registry for sharing typed context instances across component boundaries.
*
* Entries are keyed by the runtime constructor of each registered instance:
* - one active entry per concrete constructor
* - registering another instance of the same constructor replaces the previous
*
* Lookup is type-based and uses `instanceof`, so querying a base class can
* resolve a registered subclass instance.
*/
export class ContextRegistry {
private readonly byConstructor = new Map<ContextClass<object>, object>()

/**
* Registers a context instance under its concrete runtime constructor.
*
* If an instance with the same constructor already exists, it is replaced.
*
* @param context - Context instance to register.
*/
register<TContext extends object>(context: TContext): void {
this.byConstructor.set(context.constructor as ContextClass<object>, context)
}

/**
* Removes the entry for a constructor.
*
* No-op when the constructor is not registered.
*
* @param contextClass - Constructor key to remove.
*/
unregisterByClass<TContext extends object>(
contextClass: ContextClass<TContext>,
): void {
this.byConstructor.delete(contextClass)
}

/**
* Removes a specific context instance if it is currently registered for its
* constructor.
*
* This prevents deleting a newer replacement instance of the same class.
*
* @param context - Context instance to remove.
*/
unregister<TContext extends object>(context: TContext): void {
const key = context.constructor as ContextClass<object>
if (this.byConstructor.get(key) === context) {
this.byConstructor.delete(key)
}
}

/**
* Finds a context instance by constructor type.
*
* The registry is scanned in insertion order and each entry is checked with
* `instanceof contextClass`.
*
* @param contextClass - Class to match via `instanceof`.
* @returns Matching instance, or `undefined` when not found.
*/
find<TContext extends object>(
contextClass: ContextClass<TContext>,
): TContext | undefined {
for (const value of this.byConstructor.values()) {
if (value instanceof contextClass) return value
}
return undefined
}

/**
* Resolves a context instance by constructor type and guarantees a value.
*
* @param contextClass - Class to match via `instanceof`.
* @returns Matching context instance.
* @throws Error when no matching context is registered.
*/
require<TContext extends object>(
contextClass: ContextClass<TContext>,
): TContext {
const value = this.find(contextClass)
if (value) return value
throw new Error(
`${contextClass.name} is not registered in ContextRegistry.`,
)
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export { getBindData } from './cleanup/getBindData'
export { drainUnbind } from './cleanup/removeNode'
export { removeNode } from './cleanup/removeNode'
export { unbind } from './cleanup/unbind'
export { ContextRegistry } from './composition/ContextRegistry'
export { onMounted } from './composition/onMounted'
export { onUnmounted } from './composition/onUnmounted'
export { useScope } from './composition/useScope'
Expand Down
Loading