diff --git a/docs-site/src/content/docs/api/createComponent.md b/docs-site/src/content/docs/api/defineComponent.md similarity index 88% rename from docs-site/src/content/docs/api/createComponent.md rename to docs-site/src/content/docs/api/defineComponent.md index 2eb6e1c..979267a 100644 --- a/docs-site/src/content/docs/api/createComponent.md +++ b/docs-site/src/content/docs/api/defineComponent.md @@ -4,13 +4,13 @@ title: defineComponent ## Overview -The `defineComponent` function is used to create a Regor component, which encapsulates a part of your user interface (UI) with its own behavior and template. Components allow you to build complex UIs by composing smaller, reusable units. +The `defineComponent` function is used to define a Regor component, which encapsulates a part of your user interface (UI) with its own behavior and template. Components allow you to build complex UIs by composing smaller, reusable units. ## Usage -### Creating a Regor Component +### Defining a Regor Component -To create a Regor component, call the `defineComponent` function with the following parameters: +To define a Regor component, call the `defineComponent` function with the following parameters: - `template` (required): An HTML string or an object specifying the template for rendering the component. It can include the following properties: @@ -26,6 +26,9 @@ To create a Regor component, call the `defineComponent` function with the follow - `head.entangle`: Keeps refs defined in the component context entangled with `head.props` refs. Defaults to `true`. - `head.enableSwitch`: Enables slot context switching to the parent. Defaults to `false`. - `head.onAutoPropsAssigned`: Callback invoked after auto props get assigned to the component context. + - `head.findContext(ContextClass, occurrence?)`: Finds a parent context instance by `instanceof` from the captured context stack and returns `undefined` when missing. + - `head.requireContext(ContextClass, occurrence?)`: Resolves a parent context instance by `instanceof` from the captured context stack and throws if the selected occurrence is missing. + - `head.unmount()`: Unmounts this component range and runs unmounted handlers for captured contexts. ### Example diff --git a/docs-site/src/content/docs/guide/components.md b/docs-site/src/content/docs/guide/components.md index 3c4608e..d2af8df 100644 --- a/docs-site/src/content/docs/guide/components.md +++ b/docs-site/src/content/docs/guide/components.md @@ -6,7 +6,7 @@ sidebar: This guide documents how Regor components work in runtime, based on current implementation and tests. -## Create a Component +## Define a Component ```ts import { defineComponent, html } from 'regor' @@ -56,7 +56,15 @@ Component context is created by `options.context(head)`. 4. `head.entangle` (default `true`): if both sides are refs, parent and component refs are two-way entangled during auto-props. 5. `head.enableSwitch` (default `false`): enables slot context switching to parent for slot templates. 6. `head.onAutoPropsAssigned`: callback after auto props assignment. -7. `head.unmount()`: removes mounted nodes in component range and calls unmounted hooks. +7. `head.findContext(ContextClass, occurrence?)`: returns matching parent context instance from `head.ctx` by `instanceof`, or `undefined`. +8. `head.requireContext(ContextClass, occurrence?)`: resolves matching parent context instance from `head.ctx` by `instanceof`; throws if the selected occurrence does not exist. +9. `head.unmount()`: removes mounted nodes in component range and calls unmounted hooks. + +`occurrence` is zero-based: + +1. `0` (default): first match +2. `1`: second match +3. `2`: third match ### Emit Example @@ -78,6 +86,24 @@ In parent: ``` +### Parent context lookup example + +```ts +class AppServices { + api = '/v1' +} + +class OuterLayoutContext {} + +const Child = defineComponent('
', { + context: (head) => { + const services = head.requireContext(AppServices) + const secondLayout = head.findContext(OuterLayoutContext, 1) + return { services, secondLayout } + }, +}) +``` + ## How Component Inputs Are Routed On a component host tag, Regor routes bindings through two different channels: diff --git a/docs-site/src/content/docs/guide/typescript.md b/docs-site/src/content/docs/guide/typescript.md index 2b61ef5..da1e4a6 100644 --- a/docs-site/src/content/docs/guide/typescript.md +++ b/docs-site/src/content/docs/guide/typescript.md @@ -102,6 +102,25 @@ You can type `head` in class constructors and use runtime behavior flags intenti 1. `head.autoProps` to disable automatic parent-input assignment. 2. `head.entangle` to choose two-way ref sync vs snapshot behavior. 3. `head.enableSwitch` for parent-context slot evaluation. +4. `head.findContext(ServiceClass, occurrence?)` for optional parent context lookup. +5. `head.requireContext(ServiceClass, occurrence?)` for required parent context lookup. + +Example: + +```ts +class AppServices { + readonly apiBase = '/v1' +} + +class ChildContext { + apiBase: string + + constructor(head: ComponentHead) { + const services = head.requireContext(AppServices) + this.apiBase = services.apiBase + } +} +``` ## See Also diff --git a/package.json b/package.json index 07da839..326872b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "regor", - "version": "1.3.7", + "version": "1.3.8", "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", diff --git a/src/app/ComponentHead.ts b/src/app/ComponentHead.ts index 7529694..4cb4343 100644 --- a/src/app/ComponentHead.ts +++ b/src/app/ComponentHead.ts @@ -2,6 +2,8 @@ import { type IRegorContext } from '../api/types' import { removeNode } from '../cleanup/removeNode' import { callUnmounted } from '../composition/callUnmounted' +export type ContextClass = abstract new (...args: never[]) => TValue + /** * Runtime metadata passed to a component's `context(head)` factory. * @@ -160,6 +162,73 @@ export class ComponentHead< ) } + /** + * Finds a parent context instance by constructor type from the captured + * context stack. + * + * Matching uses `instanceof` and respects stack order. + * + * `occurrence` selects which matching instance to return: + * - `0` (default): first match + * - `1`: second match + * - `2`: third match + * - negative values: always `undefined` + * + * Example: + * ```ts + * // stack: [RootCtx, ParentCtx, ParentCtx] + * head.findContext(ParentCtx) // first ParentCtx + * head.findContext(ParentCtx, 1) // second ParentCtx + * ``` + * + * @param constructor - Class constructor used for `instanceof` matching. + * @param occurrence - Zero-based index of the matching instance to return. + * @returns The matching parent context instance, or `undefined` when not found. + */ + findContext( + constructor: ContextClass, + occurrence = 0, + ): TValue | undefined { + if (occurrence < 0) return undefined + let current = 0 + for (const value of this.ctx ?? []) { + if (!(value instanceof constructor)) continue + if (current === occurrence) return value + ++current + } + return undefined + } + + /** + * Returns a parent context instance by constructor type from the captured + * context stack. + * + * The stack is scanned in order and each entry is checked with `instanceof`. + * `occurrence` is zero-based (`0` = first match, `1` = second match, ...). + * If no instance exists at the requested occurrence, this method throws. + * + * Example: + * ```ts + * const auth = head.requireContext(AuthContext) // first AuthContext + * const outer = head.requireContext(LayoutCtx, 1) // second LayoutCtx + * ``` + * + * @param constructor - Class constructor used for `instanceof` matching. + * @param occurrence - Zero-based index of the instance to return. + * @returns The parent context instance at the requested occurrence. + * @throws Error when no matching instance exists at the requested occurrence. + */ + requireContext( + constructor: ContextClass, + occurrence = 0, + ): TValue { + const value = this.findContext(constructor, occurrence) + if (value !== undefined) return value + throw new Error( + `${constructor} was not found in the context stack at occurrence ${occurrence}.`, + ) + } + /** * Unmounts this component instance by removing nodes between `start` and `end` * and calling unmount lifecycle handlers for captured contexts. diff --git a/tests/app/component-head.spec.ts b/tests/app/component-head.spec.ts index 4d1b746..712a1f7 100644 --- a/tests/app/component-head.spec.ts +++ b/tests/app/component-head.spec.ts @@ -49,3 +49,48 @@ test('component head unmount removes nodes between markers and calls unmounted h expect(host.contains(n1)).toBe(false) expect(stop).toHaveBeenCalledTimes(1) }) + +test('component head findContext returns first matching parent context', () => { + class ParentContext { + constructor(readonly value: number) {} + } + class OtherContext { + readonly name = 'other' + } + + const host = document.createElement('div') + const start = document.createComment('s') + const end = document.createComment('e') + const other = new OtherContext() + const parent1 = new ParentContext(1) + const parent2 = new ParentContext(2) + const head = new ComponentHead( + {}, + host, + [other, parent1, parent2 as any], + start, + end, + ) + + expect(head.findContext(ParentContext)).toBe(parent1) + expect(head.findContext(ParentContext, 1)).toBe(parent2) + expect(head.findContext(ParentContext, 2)).toBeUndefined() + expect(head.findContext(ParentContext, -1)).toBeUndefined() + expect(head.findContext(Date)).toBeUndefined() +}) + +test('component head requireContext throws when parent context is missing', () => { + class ParentContext {} + + const host = document.createElement('div') + const start = document.createComment('s') + const end = document.createComment('e') + const head = new ComponentHead({}, host, [], start, end) + + expect(() => head.requireContext(ParentContext)).toThrow( + `${ParentContext} was not found in the context stack at occurrence 0.`, + ) + expect(() => head.requireContext(ParentContext, 1)).toThrow( + `${ParentContext} was not found in the context stack at occurrence 1.`, + ) +})