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
74 changes: 37 additions & 37 deletions docs-site/src/content/docs/guide/index.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
---
title: Guide
sidebar:
order: 4
---

The Guide explains how Regor behaves in real runtime usage, with patterns that map directly to production code.

## Guide Map

1. [Reactivity](/guide/reactivity)
Core primitives (`ref`, `sref`, computed, observers), update semantics, and state design.

2. [Templates and Expressions](/guide/templates)
Interpolation transform, parser/evaluator capabilities, and template authoring patterns.

3. [Components](/guide/components)
Component creation, input channels (`:x`, `r-bind:x`, `:context`, `r-context`), slots, and inherit behavior.

4. [TypeScript](/guide/typescript)
Strong typing patterns for app contexts, class-based contexts, and `ComponentHead<T>`.

5. [Lifecycle and Cleanup](/guide/lifecycle-and-cleanup)
Mounted/unmounted hooks, `unmount` vs `unbind`, and `drainUnbind` usage.

6. [Mounting](/guide/mounting)
Static-first + dynamic islands and existing-markup binding patterns.

7. [Persisted State](/guide/persisted-state)
`persist(...)` patterns for durable reactive state.

8. [Performance](/guide/performance)
Benchmark methodology, bottleneck analysis, and optimization workflow.

9. [Troubleshooting](/guide/troubleshooting)
Symptom -> cause -> fix guide for common runtime issues.

---
title: Guide
sidebar:
order: 4
---
The Guide explains how Regor behaves in real runtime usage, with patterns that map directly to production code.
## Guide Map
1. [Reactivity](/guide/reactivity)
Core primitives (`ref`, `sref`, computed, observers), update semantics, and state design.
2. [Templates and Expressions](/guide/templates)
Interpolation behavior, parser/evaluator capabilities, and template authoring patterns.
3. [Components](/guide/components)
Component creation, input channels (`:x`, `r-bind:x`, `:context`, `r-context`), slots, and inherit behavior.
4. [TypeScript](/guide/typescript)
Strong typing patterns for app contexts, class-based contexts, and `ComponentHead<T>`.
5. [Lifecycle and Cleanup](/guide/lifecycle-and-cleanup)
Mounted/unmounted hooks, `unmount` vs `unbind`, and `drainUnbind` usage.
6. [Mounting](/guide/mounting)
Static-first + dynamic islands and existing-markup binding patterns.
7. [Persisted State](/guide/persisted-state)
`persist(...)` patterns for durable reactive state.
8. [Performance](/guide/performance)
Benchmark methodology, bottleneck analysis, and optimization workflow.
9. [Troubleshooting](/guide/troubleshooting)
Symptom -> cause -> fix guide for common runtime issues.
9 changes: 4 additions & 5 deletions docs-site/src/content/docs/guide/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@ Regor supports two interpolation delimiters:
<p>[[ user.name ]]</p>
```

## Interpolation Transform Rules
## Interpolation Behavior

When interpolation is enabled (`RegorConfig.useInterpolation = true`):

1. Interpolation in text nodes is converted to directive bindings before normal bind phase.
2. If an element contains exactly one interpolation token with only surrounding whitespace, Regor rewrites that element to use `r-text`.
3. Mixed text + interpolation is split into text nodes and generated `<span r-text="...">` nodes.
4. Subtrees marked with `r-pre` are skipped.
1. Interpolation expressions in text nodes become reactive text.
2. Interpolation works as the whole text content or mixed with surrounding text.
3. Subtrees marked with `r-pre` are skipped.

If `useInterpolation = false`, `{{ ... }}` and `[[ ... ]]` stay as plain text.

Expand Down
122 changes: 61 additions & 61 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,61 +1,61 @@
{
"name": "regor",
"version": "1.7.2",
"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",
"type": "module",
"main": "dist/regor.es2022.esm.js",
"module": "dist/regor.es2022.esm.js",
"types": "dist/regor.d.ts",
"scripts": {
"build": "yarn tsx scripts/bundle.ts && yarn dts",
"bench:build": "yarn tsx benchmarks/build.ts",
"bench:serve": "yarn bench:build && yarn tsx benchmarks/server.ts",
"perf": "yarn tsx benchmarks/minidom/perf.ts",
"perf:record": "yarn tsx benchmarks/minidom/perf.ts record",
"perf:check": "yarn tsx benchmarks/minidom/perf.ts check",
"perf:profile": "yarn tsx benchmarks/minidom/profile.ts",
"dts": "yarn dts-bundle-generator src/index.ts -o dist/regor.d.ts && yarn tsx scripts/append-overloads.ts",
"lint": "yarn eslint src/**/*.ts tests/**/*.ts",
"watch": "yarn tsx scripts/bundle.ts --watch",
"format": "yarn prettier --write \"**/*.ts\"",
"test": "vitest",
"coverage": "yarn vitest run --coverage --coverage.include='src/**/*.ts' --coverage.reporter=text-summary --coverage.reporter=text"
},
"files": [
"dist"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/koculu/regor.git"
},
"keywords": [
"regor"
],
"bugs": {
"url": "https://github.com/koculu/regor/issues"
},
"homepage": "https://github.com/koculu/regor#readme",
"devDependencies": {
"@eslint/js": "^9.39.2",
"@stylistic/eslint-plugin": "^5.7.1",
"@types/node": "^25.2.1",
"@vitest/coverage-istanbul": "3.2.3",
"dts-bundle-generator": "^9.5.1",
"esbuild": "^0.25.5",
"eslint": "^9.39.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.5.0",
"prettier": "^3.5.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vitest": "^3.2.3"
},
"packageManager": "yarn@4.9.2",
"resolutions": {
"typescript": "^5.9.3"
}
}
{
"name": "regor",
"version": "1.7.3",
"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",
"type": "module",
"main": "dist/regor.es2022.esm.js",
"module": "dist/regor.es2022.esm.js",
"types": "dist/regor.d.ts",
"scripts": {
"build": "yarn tsx scripts/bundle.ts && yarn dts",
"bench:build": "yarn tsx benchmarks/build.ts",
"bench:serve": "yarn bench:build && yarn tsx benchmarks/server.ts",
"perf": "yarn tsx benchmarks/minidom/perf.ts",
"perf:record": "yarn tsx benchmarks/minidom/perf.ts record",
"perf:check": "yarn tsx benchmarks/minidom/perf.ts check",
"perf:profile": "yarn tsx benchmarks/minidom/profile.ts",
"dts": "yarn dts-bundle-generator src/index.ts -o dist/regor.d.ts && yarn tsx scripts/append-overloads.ts",
"lint": "yarn eslint src/**/*.ts tests/**/*.ts",
"watch": "yarn tsx scripts/bundle.ts --watch",
"format": "yarn prettier --write \"**/*.ts\"",
"test": "vitest",
"coverage": "yarn vitest run --coverage --coverage.include='src/**/*.ts' --coverage.reporter=text-summary --coverage.reporter=text"
},
"files": [
"dist"
],
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/koculu/regor.git"
},
"keywords": [
"regor"
],
"bugs": {
"url": "https://github.com/koculu/regor/issues"
},
"homepage": "https://github.com/koculu/regor#readme",
"devDependencies": {
"@eslint/js": "^9.39.2",
"@stylistic/eslint-plugin": "^5.7.1",
"@types/node": "^25.2.1",
"@vitest/coverage-istanbul": "3.2.3",
"dts-bundle-generator": "^9.5.1",
"esbuild": "^0.25.5",
"eslint": "^9.39.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-tsdoc": "^0.5.0",
"prettier": "^3.5.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vitest": "^3.2.3"
},
"packageManager": "yarn@4.9.2",
"resolutions": {
"typescript": "^5.9.3"
}
}
2 changes: 2 additions & 0 deletions src/bind/interpolation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RegorConfig } from '../app/RegorConfig'
import { getChildNodes } from '../common/common'
import { isNullOrWhitespace } from '../common/is-what'
import { rgi } from './rgi'

/**
* @internal
Expand Down Expand Up @@ -62,6 +63,7 @@ const interpolateTextNode = (
const delimiter = getInterpolationDelimiter(part, delimiters)
if (delimiter) {
const spanTag = document.createElement('span')
spanTag.setAttribute(rgi, '')
spanTag.setAttribute(
textDirective,
part.substring(
Expand Down
4 changes: 4 additions & 0 deletions src/bind/rgi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* @internal
*/
export const rgi = 'r-rgi'
40 changes: 33 additions & 7 deletions src/directives/text.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { type Directive } from '../api/types'
import { type BindData, type Directive } from '../api/types'
import { rgi } from '../bind/rgi'
import { bindDataSymbol } from '../cleanup/bindDataSymbol'
import { isMap, isObject, isSet } from '../common/is-what'
import { flatten } from '../misc/flatten'

type BindableNode = { [bindDataSymbol]?: BindData }

/**
* @internal
*/
const updateText = (el: HTMLElement, values: unknown[]): void => {
const updateText = (el: Node, values: unknown[]): void => {
const value = values[0]
// https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext
// Note: order is important: [isSet,isMap] should come before [isObject].
Expand All @@ -18,10 +22,32 @@ const updateText = (el: HTMLElement, values: unknown[]): void => {
: (value?.toString() ?? '')
}

const moveBindData = (from: Node, to: Node): void => {
const bindableFrom = from as BindableNode
const bindData = bindableFrom[bindDataSymbol]
if (!bindData) return
;(to as BindableNode)[bindDataSymbol] = bindData
bindableFrom[bindDataSymbol] = undefined
}

const createInterpolationTextNode = (el: HTMLElement): Node => {
if (el.tagName !== 'SPAN') return el
if (!el.hasAttribute(rgi)) return el
const parent = el.parentNode
if (!parent) return el
const textNode = el.ownerDocument.createTextNode('')
moveBindData(el, textNode)
parent.replaceChild(textNode, el)
return textNode
}

export const textDirective: Directive = {
mount: () => ({
update: ({ el, values }) => {
updateText(el, values)
},
}),
mount: ({ el }) => {
const target = createInterpolationTextNode(el)
return {
update: ({ values }) => {
updateText(target, values)
},
}
},
}
45 changes: 42 additions & 3 deletions tests/app/createApp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ test('click counter', () => {
<button @click="count++">click me: {{ count }}</button>`,
},
)
expect(root.querySelector('div > span')?.textContent).toBe('0')
expect(root.querySelector('div')?.textContent).toBe('count: 0')
for (let i = 0; i < 10; ++i) {
root.querySelector('button')?.click()
expect(root.querySelector('div > span')?.textContent).toBe(`${i + 1}`)
expect(root.querySelector('div')?.textContent).toBe(`count: ${i + 1}`)
}
htmlEqual(
root.innerHTML,
raw`<div>count: <span>10</span></div><button>click me: <span>10</span></button>`,
raw`<div>count: 10</div><button>click me: 10</button>`,
)
expect(root.querySelector('div > span')).toBeNull()
})

test('interpolation supports bracket syntax', () => {
Expand Down Expand Up @@ -73,6 +74,44 @@ test('interpolation supports both syntaxes at once', () => {
expect(root.textContent).toBe('hello world')
})

test('interpolation removes only generated spans and keeps cleanup anchored', () => {
const root = document.createElement('div')
const message = ref('hello')
const title = ref('title')
const app = createApp(
{
message,
title,
},
{
element: root,
template: html`<section>
<p>Hello {{ message }}!</p>
<span id="manual" r-text="title"></span>
</section>`,
},
)

const paragraph = root.querySelector('p') as HTMLParagraphElement
const manual = root.querySelector('#manual') as HTMLSpanElement

expect(paragraph.textContent).toBe('Hello hello!')
expect(paragraph.querySelector('span')).toBeNull()
expect(manual.tagName).toBe('SPAN')
expect(manual.textContent).toBe('title')

message('world')
title('next')
expect(paragraph.textContent).toBe('Hello world!')
expect(manual.textContent).toBe('next')

app.unbind()
message('stopped')
title('stopped')
expect(paragraph.textContent).toBe('Hello world!')
expect(manual.textContent).toBe('next')
})

test('createApp mounts json template and supports unbind', () => {
const root = document.createElement('div')
root.appendChild(document.createElement('span'))
Expand Down
Loading
Loading