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
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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

Expand Down
30 changes: 28 additions & 2 deletions docs-site/src/content/docs/guide/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand All @@ -78,6 +86,24 @@ In parent:
<Card @save="onSave($event)" />
```

### Parent context lookup example

```ts
class AppServices {
api = '/v1'
}

class OuterLayoutContext {}

const Child = defineComponent('<div></div>', {
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:
Expand Down
19 changes: 19 additions & 0 deletions docs-site/src/content/docs/guide/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>) {
const services = head.requireContext(AppServices)
this.apiBase = services.apiBase
}
}
```

## See Also

Expand Down
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.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",
Expand Down
69 changes: 69 additions & 0 deletions src/app/ComponentHead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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

/**
* Runtime metadata passed to a component's `context(head)` factory.
*
Expand Down Expand Up @@ -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<TValue>(
constructor: ContextClass<TValue>,
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<TValue>(
constructor: ContextClass<TValue>,
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.
Expand Down
45 changes: 45 additions & 0 deletions tests/app/component-head.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
)
})