Components
Compose typed components with reactive props, named slots, and explicit context boundaries.
This guide documents how Regor components work in runtime, based on current implementation and tests.
Define a reusable view, then decide which state belongs to its parent.
Try it live
Ada Lovelace
Engineer
import {
computed,
defineComponent,
html,
ref,
unref,
type RefOrValue,
} from 'regor'
interface ProfileProps {
name: RefOrValue<string>
role: RefOrValue<string>
compact: RefOrValue<boolean>
}
export function createProfile(interactive = false) {
const ProfileCard = defineComponent<
ProfileProps & { initials: RefOrValue<string> }
>(
html` <article class="guide-profile" :class="{ 'is-compact': compact }">
<div class="guide-avatar" aria-hidden="true">{{ initials }}</div>
<div>
<h3>{{ name || 'Your name' }}</h3>
<p>{{ role }}</p>
</div>
<footer><slot name="footer"></slot></footer>
</article>`,
{
props: ['name', 'role', 'compact'],
context: (head) => {
head.enableSwitch = true // The named slot reads the parent's context.
return {
...head.props,
initials: computed(
() =>
unref(head.props.name)
.trim()
.split(/\s+/)
.slice(0, 2)
.map((word) => word[0] ?? '')
.join('')
.toUpperCase() || '?',
),
}
},
},
)
return {
interactive,
name: ref('Ada Lovelace'),
role: ref('Engineer'),
compact: ref(false),
available: ref(true),
components: { ProfileCard },
}
}
export const profileTemplate = html` <div class="guide-demo guide-demo--split">
<div class="guide-controls">
<label for="profile-name">Display name</label
><input
id="profile-name"
type="text"
maxlength="40"
r-model="name"
:disabled="!interactive"
/>
<label for="profile-role">Role</label
><select id="profile-role" r-model="role" :disabled="!interactive">
<option>Engineer</option>
<option>Designer</option>
<option>Researcher</option>
</select>
<label class="guide-check"
><input type="checkbox" r-model="compact" :disabled="!interactive" />
Compact layout</label
>
<label class="guide-check"
><input type="checkbox" r-model="available" :disabled="!interactive" />
Available for a project</label
>
</div>
<div class="guide-profile-preview">
<span class="guide-kicker">CHILD COMPONENT / LIVE PROPS</span>
<ProfileCard :name="name" :role="role" :compact="compact">
<template #footer
><span class="guide-slot-label">PARENT SLOT</span
><span
>{{ available ? 'Open to collaboration' : 'Focused on a project'
}}</span
></template
>
</ProfileCard>
</div>
</div>` import { createApp, useScope } from 'regor'
import { createQuote, quoteTemplate } from './quote'
import { createServices, servicesTemplate } from './services'
import { createProfile, profileTemplate } from './profile'
import { createLifecycle, lifecycleTemplate } from './lifecycle'
// Each guide mounts only its own island. Everything outside the root stays static.
function mount<T extends object>(
id: string,
create: (interactive: boolean) => T,
template: string,
) {
const element = document.getElementById(id)
if (element)
createApp(
useScope(() => create(true)),
{ element, template },
)
}
mount('guide-quote', createQuote, quoteTemplate)
mount('guide-services', createServices, servicesTemplate)
mount('guide-profile', createProfile, profileTemplate)
mount('guide-lifecycle', createLifecycle, lifecycleTemplate) The parent owns the form. The child receives live props, computes initials, and renders a named slot supplied by the parent.
Define a Component
import { defineComponent, html } from 'regor'
export const UserCard = defineComponent(
html`<article><h3 r-text="title"></h3></article>`,
{ props: ['title'] },
) defineComponent(template, options) supports:
- Template from string:
defineComponent('<div>...</div>') - Template object with
template - Template object with
element - Template object with
selector - Template object with
json
Interpolation is enabled by default (useInterpolation: true).
Register and Use Components
Register via app context:
createApp({
components: { UserCard },
}) or via RegorConfig.addComponent(...) (registered/global style).
Tags resolve in component map using component name and kebab-case variant.
<UserCard :title="activeUser.name" /> <user-card :title="activeUser.name" /> Component Context and ComponentHead
Component context is created by options.context(head). head is ComponentHead and exposes:
head.props: resolved incoming props object.head.emit(event, detail): dispatchesCustomEventfrom host element.head.autoProps(defaulttrue): auto-assigns props into component context fields.head.entangle(defaulttrue): if both sides are refs, parent and component refs are two-way entangled during auto-props.head.enableSwitch(defaultfalse): enables slot context switching to parent for slot templates.head.onAutoPropsAssigned: callback after auto props assignment.head.findContext(ContextClass, occurrence?): returns matching parent context instance fromhead.ctxbyinstanceof, orundefined.head.requireContext(ContextClass, occurrence?): resolves matching parent context instance fromhead.ctxbyinstanceof; throws if the selected occurrence does not exist.head.validateProps(schema): validates selected incoming props at runtime.head.unmount(): removes mounted nodes in component range and calls unmounted hooks.
occurrence is zero-based:
0(default): first match1: second match2: third match
Emit Example
class CardContext {
title = ref('local')
$emit?: (event: string, args: Record<string, unknown>) => void
save = () => this.$emit?.('save', { title: this.title() })
}
const Card = defineComponent('<button @click="save">Save</button>', {
context: () => new CardContext(),
}) In parent:
<Card @save="onSave($event)" /> Parent context lookup example
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 }
},
}) Runtime Prop Validation
Regor lets component authors validate incoming props inside context(head).
This validation model is intentionally runtime-first:
- It is fully opt-in.
- It validates only the keys you list.
- It follows the active
propValidationModeconfig. - It does not coerce values.
- It does not mutate
head.props.
Use the built-in validators through pval:
import { defineComponent, html, pval } from 'regor'
type EditorCard = {
title: string
count?: number
mode: 'create' | 'edit'
summary?: string
}
const EditorCard = defineComponent<EditorCard>(
html`<article>{{ summary }}</article>`,
{
props: ['title', 'count', 'mode'],
context: (head) => {
head.validateProps({
title: pval.isString,
count: pval.optional(pval.isNumber),
mode: pval.oneOf(['create', 'edit'] as const),
})
return {
...head.props,
summary: `${head.props.title}:${head.props.mode}:${head.props.count ?? 'none'}`,
}
},
},
) Built-in validators
pval currently provides:
pval.isStringpval.isNumberpval.isBooleanpval.isClass(SomeClass)pval.optional(validator)pval.nullable(validator)pval.or(...validators)pval.oneOf([...])pval.arrayOf(validator)pval.shape({ ... })pval.refOf(validator)pval.describe(value)pval.fail(name, detail)
Example:
head.validateProps({
title: pval.isString,
count: pval.optional(pval.isNumber),
mode: pval.oneOf(['create', 'edit'] as const),
value: pval.or(pval.isString, pval.isNumber),
tags: pval.arrayOf(pval.isString),
meta: pval.shape({
slug: pval.isString,
retries: pval.nullable(pval.isNumber),
}),
}) Single-prop bindings and refs
Dynamic single-prop bindings such as :title="titleRef" flow into component props as refs. When validating that runtime value, use pval.refOf(...):
type TitleCard = {
title: Ref<string>
summary?: string
}
const TitleCard = defineComponent<TitleCard>(html`<h3>{{ summary }}</h3>`, {
props: ['title'],
context: (head) => {
head.validateProps({
title: pval.refOf(pval.isString),
})
return {
...head.props,
summary: head.props.title(),
}
},
}) Object input validation
For object-style :context="{ ... }" values, validate the plain runtime object shape:
head.validateProps({
meta: pval.shape({
slug: pval.isString,
}),
}) Custom validators
Users are not limited to the built-in validators. Any function matching PropValidator<T> can be used:
import { pval, type PropValidator } from 'regor'
const isNonEmptyString: PropValidator<string> = (value, name) => {
if (typeof value !== 'string' || value.trim() === '') {
pval.fail(name, `expected non-empty string, ${pval.describe(value)}`)
}
}
head.validateProps({
title: isNonEmptyString,
}) Custom validators can also use the third head argument:
const startsWithParentPrefix: PropValidator<string> = (value, name, head) => {
const ctx = head.requireContext(AppServices)
if (typeof value !== 'string' || !value.startsWith(ctx.prefix)) {
pval.fail(name, `expected prefixed value, ${pval.describe(value)}`)
}
} When to use validation
Validation is most useful when:
- The component depends on a required runtime contract.
- The same component is used in many places.
- You want an explicit runtime guard before local state mapping.
- You set
head.autoProps = falseand map incoming props manually.
Validation mode
Validation behavior is configured through RegorConfig.propValidationMode:
import { RegorConfig } from 'regor'
const config = new RegorConfig()
config.propValidationMode = 'warn' Modes:
'throw'(default): throws on the first invalid prop.'warn': sends the validation failure towarningHandler.warning(...)and continues.'off': skips runtime prop validation.
Pass the config into createApp(...) to apply the mode for that app:
createApp(appContext, template, config) How Component Inputs Are Routed
On a component host tag, Regor routes bindings through two different channels:
- Component input channel: values end up in
head.propsand can become component state. - Attribute fallthrough channel: values are copied as DOM attributes/class/style to rendered output root.
1) Declared single-prop bindings (component input channel)
Use:
:x="...".x="..."r-bind:x="..."
These are treated as component props only if x is declared in props: [...].
const Card = defineComponent('<h3 r-text="title"></h3>', {
props: ['title'],
context: (head) => ({ title: head.props.title }),
}) <Card :title="user.name"></Card> <Card r-bind:title="user.name"></Card> If x is not declared in props, that single binding is treated as normal attribute fallthrough.
2) Object component input (:context / r-context)
Use:
<Card :context="{ title: user.name, badge: role }"></Card>
<Card r-context="{ title: user.name, badge: role }"></Card> This is object-style component input and does not require keys to be listed in props.
3) Object-form r-bind uses the attribute channel
<Card r-bind="{ id: cardId, 'data-role': role }"></Card> For component hosts, this form is processed as attribute forwarding to rendered output nodes. It does not go through declared-prop resolution or :context object assignment.
r-bind:x="..." is different: it is single-key binding and can map to component input when x is declared in props.
autoProps and entangle behavior
head.autoProps = true: Regor mergeshead.propsinto the returned component context.- With
head.autoProps = true, omitted declared prop names are added as localundefinedonly when the component did not already define that field. - That keeps declared prop names local to the component and prevents same-named parent values from being resolved by accident.
head.autoProps = false: component author maps fromhead.propsmanually, and omitted names are not isolated automatically.head.autoProps = true+head.entangle = true: ref-to-ref inputs are two-way entangled.head.autoProps = true+head.entangle = false: ref-to-ref inputs are initial snapshot only.- Primitive/object values targeting existing component ref fields are applied to those refs.
- For single-prop bindings, wrapping an expression in
ref(...)gives the child a live reactive prop source even when the expression evaluates to a non-ref.
Slots
Supported slot patterns:
- Default slot:
<slot></slot> - Named slot:
<slot name="extra"></slot> - Named slot shorthand in component template:
<slot #extra></slot> - Host-side named template:
<template name="extra">...</template>or<template #extra>...</template> - Fallback slot content when host does not provide matching content.
Default slot behavior:
- If host provides unnamed content, it is used.
- Named-only template shortcuts are not injected into default slot.
For parent-context slot expression switching, enable:
context: (head) => {
head.enableSwitch = true
return {}
} Set head.enableSwitch = true when slot expressions should resolve against the parent. The preview uses this for the availability footer.
Attribute inheritance (inheritAttrs)
By default, component host attributes are inherited into component output root (inheritAttrs: true).
Details:
classis merged.styleproperties are merged.- Other attributes are copied.
:contextis excluded from inherit copy.- If component has multiple root elements,
r-inheritcan mark intended inheritor.
Set inheritAttrs: false to disable this behavior.
Dynamic components with :is
<div :is="currentCard" :item="item"></div> Switching :is remounts target component selection while keeping reactive prop flow behavior.
Nested components and lifecycle
Nested component trees are supported (r-for, r-if, nested slots). Component lifecycle hooks run through standard mounted/unmounted flow. Unmount cleans child component bindings and observers.
Best Practices
- Declare single props in
propswhen using individual bindings. - Use
:contextfor object-style prop passing. - Use
head.validateProps(...)for explicit runtime prop contracts. - Use
pval.refOf(...)for dynamic single-prop bindings that arrive as refs. - Use
head.enableSwitch = truewhen slot content must evaluate in parent context. - Decide
autoProps+entangleexplicitly when designing parent-child data ownership. - Keep fallback slot content for robust component defaults.
- Use stable keys for component lists rendered with
r-for.